Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

Admin Print Action Extensions

Admin print actions add a “Print” button to an admin resource page that produces a print-ready document for that resource — packing slips, branded invoices, gift receipts, return forms, pick lists, anything that ends in a window.print() call. Unlike Admin Actions (which open an interactive modal), print actions exist for one job: render a printable view and hand it to the browser’s native print dialog. The host page automatically opens a new window, drops the rendered HTML into it, and triggers window.print() as soon as the page paints.
The current build renders printable HTML and uses the browser’s native print dialog. Headless-Chrome PDF generation, page-size hints, and direct receipt-printer support are tracked as future work — see Roadmap.

How it works

The host (TeamInfra admin) opens the response in a new window:
  • Template mode — the response includes html already wrapped in a print-ready envelope (with <script>window.print()</script> baked in). The new window writes the HTML, the script fires on load, and the print dialog opens automatically.
  • Iframe mode — the host points the new window at your iframeUrl. Your app is responsible for fetching whatever it needs, rendering the printable view, and calling window.print() itself.
If the merchant cancels the print dialog, the new window stays open and the user can re-trigger printing manually. There is no callback to your app for cancellation.

Two rendering modes

You choose one of these per print action. The choice is dictated by which field you set on the manifest: template (string of Aqua/Liquid) or appUrl (HTTPS URL).

Template mode

Ship an Aqua/Liquid template string. CustomerLMS renders it server-side with the resource id in context and returns HTML wrapped in a print envelope. Zero app-server work at print time.

Iframe mode

Ship a URL. CustomerLMS returns a signed URL the host opens in a new window. Your app generates the printable view dynamically, fetching whatever live data it needs.
Pick template mode for layouts that depend only on the resource id and a few static fields (packing slip header, simple invoice). Pick iframe mode when the printable content depends on data outside the resource (live inventory, branded marketing pixels, customer-specific copy). A single print action must not set both template and appUrl. The server checks appUrl first; if both are set it’s used and template is ignored. Pick one.

Available targets

TargetWhere it renders
admin.order-details.print.renderPrint menu on the order detail page
Additional admin.{resource}-details.print.render targets for product, draft order, gift card, customer, and shipment pages are planned as the respective admin detail pages get wired in. Track this in the Roadmap.

Extension manifest

Each print action is one entry in extensions.printActions on your app.json. After install, each entry is persisted as a single schema file at:
public/extensions/{domainSlug}/{appHandle}/print-actions/{handle}.schema.json
This is the file the API reads at print time. Reinstalling overwrites the file; uninstalling removes the parent extension directory.

Template-mode manifest

{
  "handle": "warehouse-bundle",
  "name": "Warehouse Tools",
  "version": "1.0.0",
  "extensions": {
    "printActions": [
      {
        "handle": "packing-slip",
        "title": "Packing slip",
        "target": "admin.order-details.print.render",
        "template": "<h1>Packing slip — Order {{ order.id }}</h1><p>Pull and pack items for shipment.</p>",
        "icon": "https://warehouse-bundle.example.com/icons/box.svg"
      }
    ]
  }
}

Iframe-mode manifest

{
  "handle": "invoice-toolkit",
  "name": "Invoice Toolkit",
  "version": "1.0.0",
  "extensions": {
    "printActions": [
      {
        "handle": "branded-invoice",
        "title": "Branded invoice",
        "target": "admin.order-details.print.render",
        "appUrl": "https://invoice-toolkit.example.com/print/invoice",
        "icon": "https://invoice-toolkit.example.com/icons/invoice.svg"
      }
    ]
  }
}

Fields

FieldRequiredDescription
handleyesURL-safe identifier — unique per app. Used as the schema filename and the API handle parameter.
targetyesPrint slot. See Available targets.
titleyesPrint menu label shown to the merchant. Also used as the new window’s <title>.
templateone ofAqua/Liquid string rendered server-side with the resource context. Pick this or appUrl.
appUrlone ofHTTPS URL loaded as the printable page. Must serve a self-printing HTML view. Pick this or template.
iconnoURL of an SVG/PNG icon shown next to the menu item.
The schema persisted on disk is the manifest entry verbatim (with name and title merged for back-compat). Schema mode is detected at print time by checking appUrl first, then falling back to template.

Rendering endpoint

When the merchant clicks the print menu item, the admin host sends a POST to CustomerLMS to resolve the action:
POST /api/apps/print-action
Content-Type: application/json

{
  "domainSlug": "acme-store",
  "appHandle": "warehouse-bundle",
  "handle": "packing-slip",
  "resourceId": "ord_8a7f...",
  "resourceType": "order"
}
Required fields: domainSlug, appHandle, handle. resourceId and resourceType are surfaced to your template and as iframe query params. The endpoint is CORS-open (Access-Control-Allow-Origin: *) so the TeamInfra admin can call it cross-origin. Auth-gating happens at the admin UI layer, not on this endpoint — anyone who knows the manifest handle can hit it.

Template-mode response

{
  "type": "html",
  "html": "<!DOCTYPE html>\n<html>\n  <head>...</head>\n  <body>\n    <h1>Packing slip — Order ord_8a7f...</h1>\n    <p>Pull and pack items for shipment.</p>\n    <script>window.addEventListener('load', function () { window.print(); });</script>\n  </body>\n</html>",
  "title": "Packing slip"
}
The host pops a new window, writes the returned HTML into it, and the embedded <script>window.print()</script> fires automatically once the page paints.

Iframe-mode response

{
  "type": "iframe",
  "iframeUrl": "https://invoice-toolkit.example.com/print/invoice?resourceId=ord_8a7f...&resourceType=order&domainSlug=acme-store&handle=branded-invoice",
  "title": "Branded invoice"
}
The host opens iframeUrl in a new window. The query params are appended automatically — see Iframe-mode details.

Error responses

HTTPBodyCause
400{ error: "Missing required fields: domainSlug, appHandle, handle" }Caller didn’t pass one of the required fields.
400{ error: "Print action manifest must declare either \template` or `appUrl`” }`Manifest is on disk but has neither field set.
404{ error: "Print action manifest not found: ..." }The schema file doesn’t exist — install probably never ran.
500{ error: "Failed to render print template", details: "..." }Liquid compilation/render error.
500{ error: "Invalid print action manifest JSON", ... }The schema file on disk is corrupted.

Template-mode details

In template mode your Aqua/Liquid template is rendered server-side using the same engine that powers theme rendering. You get the full filter library (money, date, default, escape, etc.) and the full render / include / for syntax. The context exposed to the template is intentionally minimal in the current build:
{
  resource:    { id: "ord_8a7f...", type: "order" },
  order:       { id: "ord_8a7f..." },  // mirrored under resourceType for natural access
  domainSlug:  "acme-store",
  handle:      "packing-slip"
}
When resourceType is "order" you can write {{ order.id }}; when it’s "customer" you’d write {{ customer.id }}. The mirror always maps to the literal resourceType string, so an unknown resource type will surface as {{ <resourceType>.id }}.
The MVP context does not hydrate the full resource. {{ order.line_items }}, {{ order.shipping_address }}, {{ order.customer.email }} are all empty. If your template needs more than the id, use iframe mode and fetch the resource yourself.A future release will hydrate the resource via the backend so templates can render line items, addresses, and totals without an extra round trip. Until then, plan around the bare id.
The rendered HTML is wrapped in a minimal envelope:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Packing slip</title>
    <style>
      body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 24px; }
      @media print { body { padding: 0; } }
    </style>
  </head>
  <body>
    <!-- your rendered template here -->
    <script>window.addEventListener('load', function () { window.print(); });</script>
  </body>
</html>
The default @media print rule zeroes padding so your content prints edge-to-edge. Override it in your own <style> block if you need margins.

Example: packing slip template

<style>
  body { font-family: Arial, sans-serif; }
  h1 { border-bottom: 2px solid #000; padding-bottom: 8px; margin-bottom: 16px; }
  .meta { color: #666; font-size: 12px; margin-bottom: 24px; }
  .items { width: 100%; border-collapse: collapse; }
  .items th, .items td { border: 1px solid #ccc; padding: 8px; text-align: left; }
  .items th { background: #f4f4f4; }
  .footer { margin-top: 32px; font-size: 11px; color: #888; }
  @page { size: A4; margin: 12mm; }
</style>

<h1>Packing Slip</h1>
<p class="meta">Order {{ order.id }} · {{ domainSlug }}</p>

<table class="items">
  <thead>
    <tr><th>SKU</th><th>Qty</th><th>Item</th></tr>
  </thead>
  <tbody>
    {% comment %}
      MVP context exposes only the resource id. To list line items,
      switch to iframe mode and fetch from your backend.
    {% endcomment %}
    <tr>
      <td>—</td>
      <td>—</td>
      <td>Line items render here once the platform hydrates orders into the print context.</td>
    </tr>
  </tbody>
</table>

<div class="footer">
  Pulled and packed on {{ "now" | date: "%Y-%m-%d %H:%M" }} · Auto-generated.
</div>

Example: minimal invoice template

<style>
  body { font-family: Georgia, serif; max-width: 720px; }
  .invoice-header { display: flex; justify-content: space-between; align-items: flex-start; }
  .invoice-header img { height: 40px; }
  h1 { font-size: 28px; margin: 0; }
  .invoice-meta { color: #555; font-size: 13px; }
  table { width: 100%; border-collapse: collapse; margin-top: 24px; }
  th, td { padding: 10px 8px; border-bottom: 1px solid #eee; }
  .total-row td { font-weight: bold; border-top: 2px solid #000; }
</style>

<div class="invoice-header">
  <div>
    <h1>Invoice</h1>
    <p class="invoice-meta">
      Invoice for order {{ order.id }}<br />
      Issued by {{ domainSlug }}
    </p>
  </div>
  <img src="https://acme-store.example.com/logo.png" alt="Acme" />
</div>

<table>
  <thead>
    <tr><th>Item</th><th>Qty</th><th>Price</th></tr>
  </thead>
  <tbody>
    <tr><td colspan="3" style="color:#888">Line items pending platform hydration.</td></tr>
    <tr class="total-row"><td colspan="2">Total</td><td>—</td></tr>
  </tbody>
</table>

Iframe-mode details

In iframe mode your appUrl is opened in a new window with these query parameters appended:
ParameterDescription
resourceIdThe id of the resource being printed.
resourceTypeThe resource type (order, etc.).
domainSlugThe merchant’s domain slug.
handleThe print action handle.
If your appUrl already contains a query string, the params are merged. If appUrl is unparseable as a URL (relative path, missing scheme), it is returned as-is and the host opens it verbatim. Your printable page should:
  1. Read the params. They tell you which resource to render.
  2. Authenticate. If you need to call your own backend, obtain a session token through App Bridge and include it in your fetch.
  3. Render the page. Same DOM you’d use for any printable web view.
  4. Print. Call window.print() once the DOM is fully painted — typically inside a load handler or a short setTimeout so images have time to lay out.
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Invoice</title>
    <style>
      body { font-family: system-ui, sans-serif; padding: 24px; }
      @media print { body { padding: 0; } }
      h1 { margin: 0 0 16px; }
    </style>
  </head>
  <body>
    <h1>Invoice</h1>
    <div id="invoice">Loading…</div>

    <script>
      window.addEventListener('load', async () => {
        const params = new URLSearchParams(location.search);
        const order = await fetch(
          '/api/orders/' + params.get('resourceId'),
        ).then((r) => r.json());

        document.querySelector('#invoice').innerHTML = `
          <p>Order ${order.id}</p>
          <p>Total: ${order.total}</p>
        `;

        // Give the browser a tick to paint, then print.
        setTimeout(() => window.print(), 100);
      });
    </script>
  </body>
</html>

Why use iframe mode

  • You need the full resource (line items, addresses, customer fields) that template-mode context doesn’t expose yet.
  • The printable view depends on data only your service has (subscription cadence, custom inventory state, multi-warehouse pick paths).
  • You want to ship marketing/branded HTML that you control end-to-end without redeploying the merchant’s app.

Why use template mode

  • Zero app-server work at print time. CustomerLMS is the only thing online.
  • You only need the resource id stamped on a layout (gift receipt with no prices, simple thank-you slip).
  • You want offline-friendly behaviour — no need for the merchant to have working internet to your app, only to LaunchMyStore.

Installing print actions

Print actions install through the same /api/apps/install-extensions endpoint as other extension types. Add a printActions array under extensions:
curl -X POST https://store.launchmystore.io/api/apps/install-extensions \
  -H "Content-Type: application/json" \
  -d '{
    "domainSlug": "acme-store",
    "appHandle": "warehouse-bundle",
    "extensions": {
      "printActions": [
        {
          "handle": "packing-slip",
          "title": "Packing slip",
          "target": "admin.order-details.print.render",
          "template": "<h1>Packing slip — {{ order.id }}</h1>"
        },
        {
          "handle": "branded-invoice",
          "title": "Branded invoice",
          "target": "admin.order-details.print.render",
          "appUrl": "https://invoice-toolkit.example.com/print/invoice"
        }
      ]
    }
  }'
After install, each entry lives in:
public/extensions/{domainSlug}/{appHandle}/print-actions/{handle}.schema.json
The _localManifests.js reader picks these up and exposes them under admin_print_action type to the host. The host’s AdminPrintActionSlot (in TeamInfra) renders them as menu items inside the merchant’s “Print” dropdown. Subsequent installs overwrite existing schema files — there is no “add to existing” semantics. Always send your full set of print actions on each install.

Multiple print actions per app

One app can register many print actions for the same target — they all appear in the merchant’s “Print” menu and are sorted by handle. This is the typical pattern for warehouse / fulfilment apps that ship a suite (packing slip + pick list + return form + gift receipt):
{
  "extensions": {
    "printActions": [
      { "handle": "packing-slip", "title": "Packing slip", "target": "admin.order-details.print.render", "template": "..." },
      { "handle": "pick-list",    "title": "Pick list",    "target": "admin.order-details.print.render", "template": "..." },
      { "handle": "return-form",  "title": "Return form",  "target": "admin.order-details.print.render", "appUrl":  "..." },
      { "handle": "gift-receipt", "title": "Gift receipt", "target": "admin.order-details.print.render", "template": "..." }
    ]
  }
}

Use cases

  • Packing slips — pulled at the warehouse, listing what to grab before sealing the box.
  • Branded invoices — custom layouts with the merchant’s logo, terms, bank details, tax IDs.
  • Gift receipts — recipient-friendly receipts that hide prices.
  • Return labels / RMA forms — pre-filled return paperwork the customer service rep can hand to the customer.
  • Pick lists — multi-order picking sheets used by fulfilment teams.
  • Compliance documents — country-specific export forms, dangerous goods declarations, customs invoices.
Admin print actionAdmin action
OutputPrintable HTML opened in a new windowInteractive modal iframe
TriggerClick “Print” menu itemClick action button
Auto-printsYes (window.print() on load)No
Manifest fieldextensions.printActions[]extensions.adminActions[]
type fieldadmin_print_actionadmin_action
Server endpointPOST /api/apps/print-action(none — host loads iframe directly)
Template modeYes — Aqua/Liquid stringNo — iframe only
Use casesPacking slips, invoices, labelsRefunds, resync, one-shot edits

Roadmap & hardware integration

The current build is intentionally minimal — printable HTML + browser print dialog. Several extensions are tracked for future releases:
  • Headless-Chrome PDF rendering. Optional server-side flag on the manifest (renderAs: 'pdf') that returns a PDF buffer instead of HTML, so the merchant can save / email the result without re-rendering client-side.
  • Resource hydration in template mode. Expose the full order / product / customer object on the Aqua context so templates can render line items, addresses, and totals without iframe mode.
  • More targets. Print actions on the product detail page (barcode labels), draft order page (proforma invoice), gift card page (printed gift card), and shipment page (carrier label).
  • Receipt-printer / thermal-printer integration. Today the platform uses the browser print dialog only — any printer the OS exposes works (laser, inkjet, label printer, thermal receipt printer). Direct ESC/POS or ZPL output to a USB/network receipt printer (without the browser dialog round-trip) is not wired and will require a native bridge or a WebUSB-based POS-side helper. Track this in the roadmap; for now, point your thermal printer at the OS print queue and pick it in the browser dialog.
  • Print presets. Save a merchant’s preferred paper size, orientation, and margins per action so the dialog opens with the right defaults.
  • Bulk print. Select N orders in the order list and run a single print action across all of them (useful for end-of-day fulfilment).
If you need any of these sooner, raise it on the developer forum so we can prioritize.

See also