bitdreamit/laravel-qz-tray

Laravel QZ Tray is a complete silent printing solution that connects your Laravel application to desktop printers via QZ Tray. It allows you to print directly from the browser without print dialogs, with smart caching, printer memory, and automatic fallback.

Maintainers

Package info

github.com/bitdreamit/laravel-qz-tray

Language:JavaScript

pkg:composer/bitdreamit/laravel-qz-tray

Statistics

Installs: 21

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-19 09:08 UTC

This package is auto-updated.

Last update: 2026-05-19 14:10:49 UTC


README

Laravel PHP QZ Tray License

Enterprise silent printing for Laravel. Print PDFs, ZPL labels, ESC/POS receipts, and raw commands directly from your browser — no print dialog, no pop-ups — using QZ Tray's WebSocket bridge.

Table of Contents

What is This?

Standard web printing always shows a dialog box. Laravel QZ Tray eliminates that entirely.

Browser  ──HTTP──►  Laravel App  ──WebSocket──►  QZ Tray (desktop)  ──►  Printer

How it works:

  1. QZ Tray is a small Java application running on each client machine. It opens a local WebSocket server on port 8181.
  2. Your Laravel app provides a signed certificate (at /qz/certificate) so QZ Tray trusts your website.
  3. SmartPrint.js connects to QZ Tray via WebSocket and sends print jobs silently.
  4. The print job goes directly to the physical printer — no dialog, no confirmation.

Supported print types:

Type Description Example Printers
pdf PDF documents Any printer
html HTML content Any printer
zpl Zebra Programming Language Zebra ZD420, ZT230
escpos ESC/POS thermal commands Epson TM, Star Micronics
raw Raw byte commands Any raw-capable printer

Requirements

Requirement Version
PHP 8.1 or higher
Laravel 10, 11, or 12
PHP extension ext-openssl (for certificate generation)
QZ Tray (client) 2.x — installed on each machine that prints

Note: QZ Tray must be installed on every client machine (the computer connected to the printer). It does NOT need to be on your server.

Installation — Step by Step

Step 1 — Install via Composer

composer require bitdreamit/laravel-qz-tray

Laravel auto-discovers the service provider. Nothing extra needed.

Step 2 — Run the Installer Command

php artisan qz:install

This single command does everything:

  • Publishes config/qz-tray.php
  • Publishes database migrations
  • Publishes Blade views to resources/views/vendor/qz-tray/
  • Publishes JavaScript and CSS assets to public/vendor/qz-tray/
  • Generates your SSL certificate automatically

Expected output:

🚀 Installing Laravel QZ Tray Package...

📁 Publishing configuration...
🗃️  Publishing migrations...
📄 Publishing blade views...
📦 Publishing JavaScript assets...
🔐 Generating certificate...
  Generating private key...
  Creating certificate signing request...
  ✅ Certificate generated successfully!
  📄 Certificate: /var/www/html/storage/qz/digital-certificate.txt
  🔑 Private key:  /var/www/html/storage/qz/private-key.pem

✅ QZ Tray installed successfully!

Force reinstall: Add --force to overwrite existing files:

php artisan qz:install --force

Step 3 — Run Migrations

php artisan migrate

This creates the qz_print_jobs table for optional print job logging.

Step 4 — Add Scripts to Your Layout

Add these two lines to your main Blade layout (e.g. resources/views/layouts/app.blade.php), just before </body>:

<!DOCTYPE html>
<html>
<head>
    {{-- Required: CSRF token for signing requests --}}
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
    {{-- Your content here --}}

    {{-- Step 1: QZ Tray WebSocket library (CDN) --}}
    <script src="https://cdn.jsdelivr.net/npm/qz-tray@2.2.5/qz-tray.min.js"></script>

    {{-- Step 2: SmartPrint library (published asset) --}}
    <script src="{{ asset('vendor/qz-tray/js/smart-print.js') }}"></script>
</body>
</html>

Important: The <meta name="csrf-token"> tag is required. SmartPrint uses it to sign print requests.

Step 5 — Install QZ Tray on Client Machines

Each computer that will physically print needs QZ Tray installed.

Download links by OS:

Windows:  /qz/installer/windows  (or https://qz.io/download)
Linux:    /qz/installer/linux
macOS:    /qz/installer/macos

Or link directly in your app:

<a href="https://qz.io/download" target="_blank">
    Download QZ Tray
</a>

After installing, QZ Tray starts automatically with Windows/macOS and runs in the system tray. No configuration needed on the client side.

Step 6 — Verify Everything Works

  1. Check status endpoint:

    GET /qz/status
    

    Should return:

    {
      "success": true,
      "status": "operational",
      "certificate": "present",
      "private_key": "present"
    }
  2. Open the interactive test page:

    /qz/test
    

    This is the official QZ Tray demo — you can test all print types here.

  3. Open the SmartPrint demo page:

    /qz/smart
    

    Interactive page showing connection status, printer list, and live print testing.

  4. Test the signing pipeline:

    POST /qz/test-sign
    

    Should return "message": "Signing works correctly".

Configuration Reference

After publishing, edit config/qz-tray.php:

return [

    // Paths where the auto-generated certificate and key are stored
    'cert_path' => storage_path('qz/digital-certificate.txt'),
    'key_path'  => storage_path('qz/private-key.pem'),
    'cert_ttl'  => 3600, // seconds the browser may cache the cert

    // Certificate generation settings
    'certificate' => [
        'validity_days' => 7300,      // ~20 years
        'algorithm'     => 'sha256',
        'key_bits'      => 2048,
        'subject' => [
            'countryName'      => 'US',
            'organizationName' => 'My Company',
            'commonName'       => 'My App QZ Tray',
            'emailAddress'     => 'admin@myapp.com',
        ],
    ],

    // Auto-generate on first boot (safe for development, use artisan in production)
    'auto_generate_cert' => env('QZ_AUTO_GENERATE_CERT', false),

    // Allow HTTP endpoint to generate cert (disabled by default — security risk)
    'allow_public_cert_generate' => env('QZ_ALLOW_PUBLIC_CERT_GENERATE', false),

    // Default printer name (optional — users can pick via modal)
    'default_printer'           => env('QZ_DEFAULT_PRINTER'),
    'allow_printer_switch'      => true,
    'remember_printer_per_page' => true,   // Remember per URL path
    'printer_cache_duration'    => 86400,  // seconds (24 hours)

    // QZ Tray WebSocket connection
    'websocket' => [
        'host'    => env('QZ_WEBSOCKET_HOST', 'localhost'),
        'port'    => env('QZ_WEBSOCKET_PORT', 8181),  // QZ Tray default
        'retries' => 1,
        'timeout' => 10,
    ],

    // Browser fallback when QZ Tray is not running
    'fallback' => [
        'enabled'         => true,   // Open browser print dialog as fallback
        'open_in_new_tab' => true,
        'show_warning'    => true,
    ],

    // Keyboard shortcut to open printer selector
    'hotkey' => [
        'enabled'     => true,
        'combination' => 'ctrl+shift+p',
    ],

    // Route configuration
    'routes' => [
        'prefix'     => 'qz',        // All routes: /qz/...
        'middleware' => ['web'],      // Add 'auth' here to protect routes
        'throttle'   => '60,1',
    ],

    // Print job logging
    'logging' => [
        'enabled' => env('QZ_LOGGING_ENABLED', false),
        'channel' => env('QZ_LOGGING_CHANNEL', 'stack'),
        'level'   => env('QZ_LOGGING_LEVEL', 'info'),
    ],

];

Protect routes with authentication (recommended for production):

// config/qz-tray.php
'routes' => [
    'prefix'     => 'qz',
    'middleware' => ['web', 'auth'],  // Require login
],

Certificate Management

The certificate allows QZ Tray to trust your website. It is a self-signed SSL certificate generated on your server — only the public certificate is sent to the browser; the private key never leaves your server.

Generate a new certificate

php artisan qz:generate-certificate

Force regenerate (replace existing)

php artisan qz:generate-certificate --force

Show certificate details after generation

php artisan qz:generate-certificate --show

Example output:

🔐 Generating QZ Tray certificate...
  Generating private key...
  Creating certificate signing request...
✅ Certificate generated successfully!
  📄 Certificate: /var/www/storage/qz/digital-certificate.txt
  🔑 Private key:  /var/www/storage/qz/private-key.pem
  ⏳ Validity: 7300 days (20 years)

📋 Certificate Details:
  Subject:     /C=US/O=My Company/CN=My App QZ Tray
  Valid From:  2025-01-01 00:00:00
  Valid Until: 2045-01-01 00:00:00
  Algorithm:   RSA

Customize certificate subject

Edit config/qz-tray.php:

'certificate' => [
    'validity_days' => 7300,
    'algorithm'     => 'sha256',
    'key_bits'      => 2048,
    'subject' => [
        'countryName'            => 'GB',         // ISO country code
        'stateOrProvinceName'    => 'London',
        'localityName'           => 'London',
        'organizationName'       => 'Acme Corp',
        'organizationalUnitName' => 'IT Department',
        'commonName'             => 'Acme Print Service',
        'emailAddress'           => 'it@acme.com',
    ],
],

Then regenerate:

php artisan qz:generate-certificate --force

Certificate file locations

File Path Purpose
Public certificate storage/qz/digital-certificate.txt Sent to browser at /qz/certificate
Private key storage/qz/private-key.pem Signs requests on server — never exposed

Security: The storage/qz/ directory should not be web-accessible. Laravel's storage/ folder is not served by default — this is correct.

Artisan Commands

Command Description
php artisan qz:install Full install: publish + generate cert
php artisan qz:install --force Re-install, overwrite existing files
php artisan qz:install --no-cert Install without generating a certificate
php artisan qz:generate-certificate Generate SSL certificate
php artisan qz:generate-certificate --force Force regenerate certificate
php artisan qz:generate-certificate --show Show certificate details
php artisan qz:clear-cache Clear printer cache entries
php artisan qz:clear-cache --session Also clear session printer data
php artisan qz:clear-cache --all Clear everything

All Available Routes / API Endpoints

All routes are prefixed with /qz by default (configurable).

Security

Method URL Name Description
GET /qz/certificate qz.certificate Returns the public certificate (plain text)
POST /qz/sign qz.sign Signs data with SHA512 for QZ Tray verification

Status & Health

Method URL Name Description
GET /qz/status qz.status Full status: cert, key, endpoints
GET /qz/health qz.health Simple health check
GET /qz/test/connection qz.test.connection Test API connectivity
POST /qz/test-sign qz.test-sign Test signing pipeline end-to-end

Printer Management

Method URL Name Description
GET /qz/printers qz.printers Info endpoint (actual list comes via WebSocket)
POST /qz/printer qz.printer.set Remember a printer for a URL path
GET /qz/printer/{path} qz.printer.get Get remembered printer for a URL path

Print Jobs

Method URL Name Description
POST /qz/print qz.print Accept and log a print job
GET /qz/jobs qz.jobs List active print jobs
DELETE /qz/jobs/{id} qz.jobs.cancel Cancel a print job

Cache & Setup

Method URL Name Description
POST /qz/clear-cache qz.clear-cache Clear printer cache
POST /qz/setup qz.setup Setup info (cert/key status + endpoint URLs)
POST /qz/generate qz.generate Generate cert via HTTP (disabled by default)

Demo & Test Pages

Method URL Name Description
GET /qz/test qz.test Full QZ Tray demo page (all print types)
GET /qz/smart qz.smart SmartPrint interactive demo page
GET /qz/test/pdf qz.test.pdf Generate and stream a test PDF

Installer Downloads

Method URL Name Description
GET /qz/installer/windows qz.installer Windows installer info
GET /qz/installer/linux qz.installer Linux installer info
GET /qz/installer/macos qz.installer macOS installer info

Frontend Usage — SmartPrint JS

SmartPrint is the JavaScript library that lives in public/vendor/qz-tray/js/smart-print.js. It handles the WebSocket connection, signing, queue, retry, and printer memory automatically.

Method 1 — HTML Data Attributes (Zero JS)

The simplest approach. Add data-qz-print to any button or element. No JavaScript needed.

{{-- Print a PDF --}}
<button data-qz-print="/invoices/123.pdf">
    🖨 Print Invoice
</button>

{{-- Print with specific printer --}}
<button
    data-qz-print="/invoices/123.pdf"
    data-qz-printer="HP LaserJet M404"
    data-qz-copies="2">
    🖨 Print 2 Copies
</button>

{{-- Print ZPL label --}}
<button
    data-qz-print=""
    data-qz-type="zpl"
    data-qz-data="^XA^FO50,50^ADN,36,20^FDHello World^FS^XZ"
    data-qz-printer="Zebra ZD420">
    🏷 Print Label
</button>

{{-- Print with 2 second delay --}}
<button
    data-qz-print="/receipts/99.pdf"
    data-qz-type="pdf"
    data-qz-delay="2000">
    🖨 Print (2s delay)
</button>

Method 2 — JavaScript API

Full control via the SmartPrint global object.

// Print a PDF from a URL
SmartPrint.print('/invoices/123.pdf');

// Print with options
SmartPrint.print({
    url:     '/invoices/123.pdf',
    type:    'pdf',
    printer: 'HP LaserJet M404',
    copies:  2,
    profile: 'default',   // 'default', 'small' (80mm), 'label' (100x150mm)
});

// Print ZPL
SmartPrint.printZPL('^XA^FO50,50^ADN,36,20^FDHello^FS^XZ', 'Zebra ZD420');

// Print ESC/POS
SmartPrint.printESC('\x1B\x40Hello Thermal!\n\n\n', 'Epson TM-T88');

// Print raw bytes
SmartPrint.printRaw('\x1B\x40Test\n', 'raw', 'My Printer');

Method 3 — Global Shorthand Functions

These global helpers are available anywhere on the page:

// Shorthand for SmartPrint.print()
smartPrint('/invoices/123.pdf');
smartPrint('/invoices/123.pdf', { printer: 'HP', copies: 3 });

// Shorthand for SmartPrint.printZPL()
smartPrintZPL('^XA^FO50,50^FDTest^FS^XZ', 'Zebra ZD420');

// Shorthand for SmartPrint.printESC()
smartPrintESC('\x1B\x40Receipt\n\n\n', 'Epson TM');

Print Types Reference

Type data-qz-type value When to use
PDF pdf Standard documents, invoices, reports
HTML html Dynamic web content
ZPL zpl Zebra label printers
ESC/POS escpos Thermal receipt printers (Epson, Star)
Raw raw Any raw byte command

All Data Attributes Reference

Attribute Type Default Description
data-qz-print string URL of the file to print. Required for click-to-print buttons.
data-qz-auto-print string URL to print automatically when element loads.
data-qz-type string pdf Print type: pdf, html, zpl, escpos, raw
data-qz-printer string saved/default Printer name. If omitted, uses saved/default printer.
data-qz-copies number 1 Number of copies to print.
data-qz-data string Raw data for ZPL/ESC/POS/raw types (instead of URL).
data-qz-profile string default Paper profile: default, small (80mm), label (100×150mm)
data-qz-delay number 0 Milliseconds to wait before printing.

Full SmartPrint API Reference

Printing

// Print a PDF by URL
SmartPrint.print(url: string): void

// Print with full options object
SmartPrint.print(options: {
    url?:     string,    // URL of file to print
    type?:    string,    // 'pdf' | 'html' | 'zpl' | 'escpos' | 'raw'
    printer?: string,    // Printer name
    copies?:  number,    // Number of copies
    data?:    string,    // Raw data (for zpl/escpos/raw)
    profile?: string,    // 'default' | 'small' | 'label'
}): void

// Print raw/ZPL/ESC/POS data
SmartPrint.printRaw(data: string, type?: string, printer?: string): void
SmartPrint.printZPL(zpl: string, printer?: string): void
SmartPrint.printESC(escpos: string, printer?: string): void

Printer Management

// Get all available printers (async)
const printers = await SmartPrint.getPrinters();
// Returns: string[] e.g. ['HP LaserJet', 'Zebra ZD420', 'Epson TM-T88']

// Get currently selected printer
const name = SmartPrint.getCurrentPrinter();
// Returns: string | null

// Set / remember a printer
SmartPrint.setPrinter('HP LaserJet M404');          // Remember for current URL path
SmartPrint.setPrinter('HP LaserJet M404', 'global'); // Remember globally

// Open the printer picker modal
SmartPrint.showPrinterSwitcher();

Connection

// Connect to QZ Tray WebSocket
await SmartPrint.connect();

// Disconnect
await SmartPrint.disconnect();

// Check if connected
const ok = SmartPrint.isConnected(); // boolean

Queue Management

// Get current queue
const queue = SmartPrint.getQueue(); // array of job objects

// Clear the queue (cancels pending jobs)
SmartPrint.clearQueue();

// Retry a specific failed job by index
SmartPrint.retryJob(0);

// Retry all offline-buffered jobs
SmartPrint.retryOffline();

Cache

// Clear printer memory from localStorage
SmartPrint.clearCache();

Settings

SmartPrint.getSettings();
// Returns: { defaultPrinter: 'HP LaserJet' }

SmartPrint.updateSettings({ defaultPrinter: 'Epson TM-T88' });

Events Reference

Listen to events using SmartPrint.on(event, callback):

SmartPrint.on('connected', function(data) {
    console.log('QZ Tray connected. Printers:', data.printers);
});
Event Payload When it fires
connected { printers: string[] } WebSocket connection successful
connection-failed { error } Could not connect to QZ Tray
disconnected Connection closed
printers-loaded { printers: string[] } Printer list loaded from QZ Tray
printer-saved { printer, scope } User selected/saved a printer
job-queued { job } Print job added to queue
job-processing { job } Job is being sent to QZ Tray
job-completed { job } Job sent successfully
job-failed { job, error } Job failed (moved to offline buffer)
fallback-print { job } Browser print dialog opened as fallback
queue-cleared Queue was cleared
cache-cleared Printer cache was cleared
settings-updated { settings } Settings changed
ready { printers } SmartPrint fully initialized
init-failed { reason } qz-tray.min.js not loaded on page

Remove an event listener:

function onConnected(data) { /* ... */ }

SmartPrint.on('connected', onConnected);
SmartPrint.off('connected', onConnected);  // Remove

Printing Use Cases

Print a PDF Invoice

{{-- resources/views/invoices/show.blade.php --}}

@extends('layouts.app')

@section('content')
<div class="invoice-actions">
    {{-- Simple one-click print --}}
    <button
        data-qz-print="{{ route('invoices.pdf', $invoice) }}"
        data-qz-type="pdf"
        class="btn btn-primary">
        🖨 Print Invoice #{{ $invoice->number }}
    </button>

    {{-- Print 2 copies to a specific printer --}}
    <button
        data-qz-print="{{ route('invoices.pdf', $invoice) }}"
        data-qz-type="pdf"
        data-qz-copies="2"
        data-qz-printer="Office HP LaserJet"
        class="btn btn-secondary">
        🖨 Print 2 Copies
    </button>
</div>
@endsection

Your Laravel route just returns a PDF response as normal:

// routes/web.php
Route::get('/invoices/{invoice}/pdf', function (Invoice $invoice) {
    $pdf = PDF::loadView('invoices.pdf', compact('invoice'));
    return $pdf->stream('invoice-'.$invoice->number.'.pdf');
})->name('invoices.pdf');

Print a ZPL Label (Zebra)

{{-- Using data attributes --}}
<button
    data-qz-print=""
    data-qz-type="zpl"
    data-qz-printer="Zebra ZD420"
    data-qz-data="^XA
^FO50,50^ADN,36,20^FD{{ $product->name }}^FS
^FO50,100^ADN,24,14^FD{{ $product->sku }}^FS
^FO50,150^BCN,80,Y,N,N^FD{{ $product->barcode }}^FS
^XZ">
    🏷 Print Label
</button>
// Using JavaScript (dynamic ZPL from server)
fetch('/api/labels/' + productId + '/zpl')
    .then(r => r.text())
    .then(zpl => {
        SmartPrint.printZPL(zpl, 'Zebra ZD420');
    });

Laravel route returning ZPL:

Route::get('/api/labels/{product}/zpl', function (Product $product) {
    $zpl = "^XA\n"
         . "^FO50,50^ADN,36,20^FD{$product->name}^FS\n"
         . "^FO50,100^BCN,80,Y,N,N^FD{$product->barcode}^FS\n"
         . "^XZ";

    return response($zpl, 200, ['Content-Type' => 'text/plain']);
});

Print an ESC/POS Receipt (Thermal)

// Build ESC/POS receipt in JavaScript
const ESC = '\x1B';
const GS  = '\x1D';

const receipt = [
    ESC + '\x40',              // Initialize
    ESC + '\x61\x01',         // Center align
    ESC + '\x21\x30',         // Double height+width
    'MY STORE\n',
    ESC + '\x21\x00',         // Normal text
    ESC + '\x61\x00',         // Left align
    '--------------------------------\n',
    'Item 1               $10.00\n',
    'Item 2                $5.50\n',
    '--------------------------------\n',
    ESC + '\x45\x01',         // Bold on
    'TOTAL               $15.50\n',
    ESC + '\x45\x00',         // Bold off
    '\n\n\n',
    GS + '\x56\x41',          // Full cut
].join('');

SmartPrint.printESC(receipt, 'Epson TM-T88VI');

Or generate the receipt server-side and fetch it:

// Laravel controller
public function printReceipt(Order $order)
{
    $lines = [];
    $lines[] = "\x1B\x40";        // Init
    $lines[] = "\x1B\x61\x01";    // Center
    $lines[] = $order->store_name . "\n";
    $lines[] = "\x1B\x61\x00";    // Left

    foreach ($order->items as $item) {
        $lines[] = str_pad($item->name, 24) . str_pad('$'.$item->price, 8, ' ', STR_PAD_LEFT) . "\n";
    }

    $lines[] = "\n\n\n";
    $lines[] = "\x1D\x56\x41";    // Cut

    return response(implode('', $lines), 200, [
        'Content-Type' => 'application/octet-stream',
    ]);
}
fetch('/orders/{{ $order->id }}/receipt')
    .then(r => r.text())
    .then(data => SmartPrint.printESC(data, 'Epson TM-T88'));

Auto-Print on Page Load

Print automatically when the page loads, with no button click needed:

{{-- Print as soon as this element is in the DOM --}}
<div
    data-qz-auto-print="{{ route('invoices.pdf', $invoice) }}"
    data-qz-type="pdf"
    data-qz-printer="Receipt Printer">
</div>

With a delay (useful for "print after save" flows):

{{-- Print 2 seconds after page load --}}
<div
    data-qz-auto-print="{{ route('invoices.pdf', $invoice) }}"
    data-qz-delay="2000">
</div>

Using JavaScript:

// Print after 1 second
setTimeout(() => {
    SmartPrint.print('/orders/{{ $order->id }}/pdf');
}, 1000);

// Print when a condition is met
SmartPrint.on('connected', () => {
    SmartPrint.print('/invoices/{{ $invoice->id }}/pdf');
});

Print with Copies and Delay

<button
    data-qz-print="/labels/{{ $batch->id }}.pdf"
    data-qz-copies="10"
    data-qz-delay="500">
    🖨 Print 10 Labels
</button>
SmartPrint.print({
    url:     '/shipping/{{ $shipment->id }}.pdf',
    type:    'pdf',
    copies:  3,
    printer: 'Shipping Label Printer',
});

Printer Switcher Modal

Show a modal for the user to pick a printer:

{{-- Button to open the switcher --}}
<button onclick="SmartPrint.showPrinterSwitcher()">
    🖨 Change Printer
</button>

Or use the keyboard shortcut: Press Ctrl + Shift + P anywhere on the page.

The modal shows all available printers. The user clicks one and it is saved (per-page by default) in localStorage.

To set a printer programmatically:

// Set for current URL path only
SmartPrint.setPrinter('HP LaserJet M404');

// Set globally (remembered across all pages)
SmartPrint.setPrinter('HP LaserJet M404', 'global');

Print Queue with Retry

SmartPrint automatically queues jobs and processes them one by one. Jobs that fail are saved to a retry buffer.

// Queue multiple jobs — they print in order
SmartPrint.print('/invoices/1.pdf');
SmartPrint.print('/invoices/2.pdf');
SmartPrint.print('/invoices/3.pdf');

// Show what is in the queue
console.log(SmartPrint.getQueue());

// Clear the queue (cancels all pending)
SmartPrint.clearQueue();

// Retry a failed job by index
SmartPrint.retryJob(0);

// Show a queue status indicator in your UI
SmartPrint.on('job-queued',     d => updateQueueCount(SmartPrint.getQueue().length));
SmartPrint.on('job-completed',  d => updateQueueCount(SmartPrint.getQueue().length));
SmartPrint.on('queue-cleared',  ()  => updateQueueCount(0));

Add the built-in queue UI to your page:

{{-- This element is updated automatically by SmartPrint --}}
<ul id="sp-queue-list"></ul>

Offline Queue (Print When Reconnected)

If QZ Tray is offline when a job is submitted, SmartPrint saves it to localStorage. When QZ Tray reconnects, the jobs print automatically.

// Manually trigger retry of offline jobs
SmartPrint.retryOffline();

// Listen for when a job is buffered offline
SmartPrint.on('job-failed', function(data) {
    showNotification('QZ Tray offline — job saved. Will print when reconnected.');
});

Listen to Print Events

Use events to update your UI, track completions, or log to your backend:

SmartPrint.on('connected', function(data) {
    document.getElementById('printer-status').textContent = '🟢 QZ Tray Connected';
    document.getElementById('printer-name').textContent = SmartPrint.getCurrentPrinter() || 'None selected';
});

SmartPrint.on('connection-failed', function() {
    document.getElementById('printer-status').textContent = '🔴 QZ Tray Offline';
    document.getElementById('install-link').style.display = 'block';
});

SmartPrint.on('job-completed', function(data) {
    // Log to your backend
    fetch('/print-log', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        },
        body: JSON.stringify({
            url:       data.job.url,
            printer:   data.job.printer,
            completed: new Date().toISOString(),
        }),
    });
});

SmartPrint.on('fallback-print', function() {
    alert('QZ Tray is not running. Opening browser print dialog instead.');
});

Using the ZPL Adapter

The package includes a ZPL helper (public/vendor/qz-tray/js/adapters/zpl.js). Include it alongside smart-print.js:

<script src="{{ asset('vendor/qz-tray/js/adapters/zpl.js') }}"></script>
// Build a ZPL label programmatically
const label = ZPL.label()
    .text('Product Name', 50, 50, { size: 'large' })
    .text('SKU-12345', 50, 100, { size: 'medium' })
    .barcode('1234567890', 50, 150, { height: 80 })
    .build();

SmartPrint.printZPL(label, 'Zebra ZD420');

Using the ESC/POS Adapter

Include adapters/escpos.js:

<script src="{{ asset('vendor/qz-tray/js/adapters/escpos.js') }}"></script>
// Build an ESC/POS receipt
const receipt = ESCPOS.receipt()
    .initialize()
    .align('center')
    .bold(true).text('MY STORE').bold(false)
    .align('left')
    .divider()
    .line('Item 1', '$10.00')
    .line('Item 2', '$5.50')
    .divider()
    .bold(true).line('TOTAL', '$15.50').bold(false)
    .feed(3)
    .cut()
    .build();

SmartPrint.printESC(receipt, 'Epson TM-T88VI');

Laravel Backend Printing (Server-Side)

You can trigger a print from a Laravel controller — for example, after saving an order:

// app/Http/Controllers/OrderController.php

public function store(Request $request)
{
    $order = Order::create($request->validated());

    // Return page with auto-print directive
    return view('orders.created', [
        'order'    => $order,
        'autoPrint' => true,
        'printUrl'  => route('orders.pdf', $order),
    ]);
}
{{-- resources/views/orders/created.blade.php --}}

@if($autoPrint)
    <div
        data-qz-auto-print="{{ $printUrl }}"
        data-qz-type="pdf"
        data-qz-delay="500">
    </div>
@endif

Or redirect with a flash variable:

return redirect()->route('orders.show', $order)
    ->with('auto_print', route('orders.pdf', $order));
{{-- In your layout or show view --}}
@if(session('auto_print'))
    <div data-qz-auto-print="{{ session('auto_print') }}" data-qz-type="pdf"></div>
@endif

Database — Print Job Logging

The qz_print_jobs table (created by the migration) lets you log every print job.

Schema:

Column Type Description
id bigint Auto-increment primary key
tenant_id bigint (nullable) For multi-tenant apps
user_id bigint (nullable) FK to users table
printer_name string Name of the printer used
document_url string URL of the printed document
document_type string pdf, zpl, escpos, raw
copies int Number of copies
status string pending, processing, completed, failed
metadata JSON (nullable) Any extra data
processed_at timestamp When the job was sent to QZ Tray
created_at timestamp When the job was created
updated_at timestamp Last update

Example: Log a print job in your controller:

use Illuminate\Support\Facades\DB;

DB::table('qz_print_jobs')->insert([
    'user_id'      => auth()->id(),
    'printer_name' => $request->input('printer'),
    'document_url' => $request->input('url'),
    'document_type'=> $request->input('type', 'pdf'),
    'copies'       => $request->input('copies', 1),
    'status'       => 'completed',
    'metadata'     => json_encode(['order_id' => $orderId]),
    'processed_at' => now(),
    'created_at'   => now(),
    'updated_at'   => now(),
]);

Enable logging via the built-in print endpoint:

// config/qz-tray.php
'logging' => [
    'enabled' => true,
    'channel' => 'daily',
    'level'   => 'info',
],

When logging is enabled, every POST /qz/print request is written to your Laravel log.

Multi-Tenant Support

The qz_print_jobs table has a tenant_id column for multi-tenant applications.

// Filter print jobs by tenant
$jobs = DB::table('qz_print_jobs')
    ->where('tenant_id', auth()->user()->tenant_id)
    ->where('status', 'completed')
    ->orderByDesc('created_at')
    ->paginate(20);

Security

Route Protection

By default routes have only web middleware. Add auth in production:

// config/qz-tray.php
'routes' => [
    'prefix'     => 'qz',
    'middleware' => ['web', 'auth'],  // Require authenticated user
],

Or apply additional middleware:

'middleware' => ['web', 'auth', 'verified', 'role:printer'],

CSRF Protection

All POST requests from SmartPrint.js automatically include the CSRF token from the <meta name="csrf-token"> tag. No extra setup needed.

Certificate Security

  • The private key (storage/qz/private-key.pem) is only used server-side to sign requests. It is never sent to the browser.
  • The public certificate (storage/qz/digital-certificate.txt) is sent to QZ Tray so it knows to trust your domain.
  • QZ Tray uses this certificate to verify that print requests genuinely came from your application.
  • Never enable allow_public_cert_generate in production.

Rate Limiting

The signing endpoint is the most sensitive — consider adding rate limiting:

// config/qz-tray.php
'routes' => [
    'throttle' => '60,1',  // 60 requests per minute per IP
],

Environment Variables Reference

Add any of these to your .env file:

# Default printer (optional)
QZ_DEFAULT_PRINTER="HP LaserJet M404"

# WebSocket connection (defaults to localhost:8181)
QZ_WEBSOCKET_HOST=localhost
QZ_WEBSOCKET_PORT=8181

# Auto-generate certificate on first boot (dev only)
QZ_AUTO_GENERATE_CERT=false

# Allow certificate generation via HTTP (security risk — keep false)
QZ_ALLOW_PUBLIC_CERT_GENERATE=false

# Print job logging
QZ_LOGGING_ENABLED=true
QZ_LOGGING_CHANNEL=daily
QZ_LOGGING_LEVEL=info

Troubleshooting

"Certificate not found"

php artisan qz:generate-certificate

Check that storage/qz/ is writable:

chmod -R 775 storage/qz

"Could not connect — is QZ Tray running?"

  1. Install QZ Tray from qz.io/download
  2. Start QZ Tray — look for the icon in the system tray
  3. Check it's running on port 8181 (default)
  4. Make sure the browser page is served over http:// or https:// (not file://)

Connection works but prints fail

  • Open /qz/status — check certificate and private_key are both "present"
  • Open /qz/test — test the connection interactively
  • Make sure <meta name="csrf-token" content="{{ csrf_token() }}"> is in your <head>
  • Check browser console for errors

"CSRF token mismatch" on /qz/sign

Ensure the CSRF meta tag is present:

<head>
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>

And that /qz/sign is not excluded in VerifyCsrfToken:

// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    // Do NOT add 'qz/sign' here
];

Printer list is empty

The printer list comes from QZ Tray via WebSocket — not from the server. If empty:

  1. Confirm QZ Tray is running (system tray icon visible)
  2. Confirm the browser connected (check /qz/smart for status indicator)
  3. Try clicking "Refresh Printers" on /qz/smart

Printing works in development but not production (HTTPS)

QZ Tray 2.x supports HTTPS. Ensure:

  • Your site is on https://
  • The certificate at /qz/certificate loads without errors
  • QZ Tray 2.x is installed (not 1.x)

"openssl_sign failed"

The private key may be corrupted. Regenerate:

php artisan qz:generate-certificate --force

Clear all caches

php artisan qz:clear-cache --all
php artisan cache:clear
php artisan config:clear

File Structure

After running php artisan qz:install, the following files are published:

your-laravel-app/
├── config/
│   └── qz-tray.php                     ← Main configuration
│
├── storage/
│   └── qz/
│       ├── digital-certificate.txt     ← Public certificate (auto-generated)
│       └── private-key.pem             ← Private key — never expose this
│
├── database/
│   └── migrations/
│       └── ..._create_qz_print_jobs_table.php
│
├── resources/
│   └── views/
│       └── vendor/
│           └── qz-tray/
│               ├── default.blade.php   ← Full QZ demo page (/qz/test)
│               ├── smart.blade.php     ← SmartPrint demo page (/qz/smart)
│               └── example.blade.php  ← Usage examples
│
└── public/
    └── vendor/
        └── qz-tray/
            ├── js/
            │   ├── qz-tray.js          ← QZ Tray WebSocket library
            │   ├── smart-print.js      ← SmartPrint library ⭐
            │   ├── printer-status.js   ← Printer status widget
            │   ├── printer-switcher.js ← Printer switcher widget
            │   └── adapters/
            │       ├── zpl.js          ← ZPL label helper
            │       ├── escpos.js       ← ESC/POS receipt helper
            │       └── raw-print.js    ← Raw print helper
            ├── css/
            │   ├── bootstrap.min.css
            │   ├── font-awesome.min.css
            │   └── style.css
            └── fonts/
                └── (Font Awesome fonts)

Package source (inside vendor/bitdreamit/laravel-qz-tray/):

src/
├── QzTrayServiceProvider.php           ← Registers routes, commands, views
├── Http/
│   └── Controllers/
│       └── QzSecurityController.php    ← All 19 route handlers
└── Console/
    └── Commands/
        ├── InstallQzTray.php           ← php artisan qz:install
        ├── GenerateCertificate.php     ← php artisan qz:generate-certificate
        └── ClearQzCache.php            ← php artisan qz:clear-cache

Upgrade Guide

From v0.x to v1.0

  1. Update composer:

    composer update bitdreamit/laravel-qz-tray
  2. Re-publish assets (use --force to overwrite):

    php artisan vendor:publish --tag=qz-assets --force
    php artisan vendor:publish --tag=qz-config --force
  3. Regenerate certificate:

    php artisan qz:generate-certificate --force
  4. Run migrations:

    php artisan migrate
  5. Update HTML — replace old data-smart-print attributes with data-qz-print:

    {{-- Old --}}
    <button data-smart-print="/invoice.pdf">Print</button>
    
    {{-- New (both work, but qz-print is preferred) --}}
    <button data-qz-print="/invoice.pdf">Print</button>

FAQ

Q: Does this package require a license from QZ Tray? A: QZ Tray Community Edition is free for internal/self-hosted use. A commercial license is required for redistribution. See qz.io/pricing.

Q: Does this work on mobile devices? A: No. QZ Tray is a desktop application. Mobile browsers cannot connect to a local WebSocket server. Mobile devices need to use the standard browser print dialog.

Q: Can multiple users print to different printers at the same time? A: Yes. Each browser tab has its own WebSocket connection to QZ Tray on that machine. Users on different machines print to their own locally-connected printers independently.

Q: What happens if QZ Tray is not installed on a client machine? A: SmartPrint falls back to opening the browser's print dialog (if fallback.enabled = true in config). You can also listen to the connection-failed event and show a download link.

Q: Can I use this with React, Vue, or Livewire? A: Yes. SmartPrint is a plain JavaScript object on window. Call it from any framework:

// React
window.SmartPrint.print('/invoices/' + id + '.pdf');

// Vue
SmartPrint.print({ url: '/invoices/' + this.invoice.id + '.pdf', copies: 2 });

// Livewire (in @script)
SmartPrint.print('/invoices/' + $wire.invoiceId + '.pdf');

Q: Can I print to a network printer (not USB)? A: Yes. QZ Tray supports network printers. Set the printer name to the network printer's name as it appears in Windows/macOS printer settings.

Q: Is the private key secure? A: Yes — the private key lives in storage/qz/ which is not web-accessible. It is used only by PHP to sign requests, and is never sent to the client.

Q: The certificate expires after 20 years — do I need to renew it? A: Yes, but after 20 years. You can regenerate at any time with php artisan qz:generate-certificate --force — just ensure the new certificate is pushed to production.

License

MIT License — see LICENSE for details.

Support

Made with ❤️ by Bit Dream IT