Integration

How to integrate

Script tag, npm package, React hook and Vue 3 composable, plus lifecycle handling and state management.

Last updated

All four approaches use the same public SDK and cover the same flow: connect → authorise → pick a printer → upload content → print or preview → disconnect when leaving.

Getting the SDK

PrintLink serves the browser build itself; one line (global PrintLink):

<script src="http://127.0.0.1:17888/sdk/printlink.js"></script>

Plain script

<script src="http://127.0.0.1:17888/sdk/printlink.js"></script>
<script type="module">
  const client = new PrintLink.PrintLinkClient({ appName: 'My web app' });
  try {
    await client.connect();           // connect to the local PrintLink
    await client.ensureAuthorized();  // pairs on first use, waits for the user

    const { queues } = await client.listPrinters();
    const queueId = (queues.find((q) => q.isSystemDefault) ?? queues[0]).queueId;

    const job = await client.compose({ queueId }).addPdf(file).printAndWait();
    console.log('terminal:', job.status);
  } catch (err) {
    if (err.code === 'PAIRING_PENDING') {
      // Not a failure: wait for the user to accept, then retry ensureAuthorized()
    } else {
      console.warn(err.code, err.message, 'retryable =', err.retryable);
    }
  } finally {
    client.close();   // always disconnect when the page goes away
  }
</script>

React

import { useEffect, useRef, useState } from 'react';
import { PrintLinkClient } from '@printlink/sdk';
import type { PrinterQueueInfo } from '@printlink/types';

type State =
  | { status: 'connecting' }
  | { status: 'ready'; queues: PrinterQueueInfo[] }
  | { status: 'pending' }                   // waiting for the user to accept
  | { status: 'error'; code: string };

export function usePrintLink() {
  const clientRef = useRef<PrintLinkClient | null>(null);
  const [state, setState] = useState<State>({ status: 'connecting' });

  useEffect(() => {
    const client = new PrintLinkClient({ appName: 'My React App' });
    clientRef.current = client;
    let disposed = false;

    void (async () => {
      try {
        await client.connect();
        await client.ensureAuthorized();
        const { queues } = await client.listPrinters();
        if (!disposed) setState({ status: 'ready', queues });
      } catch (err) {
        const code = (err as { code?: string }).code ?? 'INTERNAL';
        if (!disposed) {
          setState(code === 'PAIRING_PENDING' ? { status: 'pending' } : { status: 'error', code });
        }
      }
    })();

    return () => {
      disposed = true;
      client.close();          // unmount always disconnects
    };
  }, []);

  return { client: clientRef, state };
}

Printing belongs in an event handler:

async function onPrint(client: PrintLinkClient, queueId: string, doc: PrintDocument) {
  const job = await client.compose({ queueId }).addDocument(doc).printAndWait();
  return job.status;   // 'completed' | 'failed' | 'canceled'
}

Vue 3

import { onMounted, onUnmounted, ref } from 'vue';
import { PrintLinkClient } from '@printlink/sdk';
import type { PrinterQueueInfo } from '@printlink/types';

export function usePrintLink() {
  const status = ref<'connecting' | 'ready' | 'pending' | 'error'>('connecting');
  const queues = ref<PrinterQueueInfo[]>([]);
  let client: PrintLinkClient | null = null;

  onMounted(async () => {
    client = new PrintLinkClient({ appName: 'My Vue App' });
    try {
      await client.connect();
      await client.ensureAuthorized();
      queues.value = (await client.listPrinters()).queues;
      status.value = 'ready';
    } catch (err) {
      status.value = (err as { code?: string }).code === 'PAIRING_PENDING' ? 'pending' : 'error';
    }
  });

  onUnmounted(() => client?.close());

  return { status, queues, client: () => client };
}

Client options

const client = new PrintLinkClient({
  appName: 'My business app',   // shown in the pairing prompt and the site list
  channel: 'erp',               // optional: distinguish apps on one machine
  endpoint: 'ws://127.0.0.1:17888/ws',  // optional, this is the default
  requestTimeoutMs: 30_000,     // optional per-request timeout
});

channel is a label for your own benefit, letting the job list and reports distinguish which app printed what (erp / wms / cashier). It must start with a lowercase letter, contain only lowercase letters, digits, _ and -, and be at most 64 characters. It plays no part in authorisation, and an invalid value is an error rather than something silently ignored.

Four things you must handle

  1. Not installed or not running: probe with PrintLink.detect() first and guide the user when installed === false, instead of failing from connect().
  2. Waiting for authorisation: PAIRING_PENDING is a normal intermediate state. Ask the user to accept on their computer, then retry ensureAuthorized().
  3. Disconnect when leaving: close() releases the connection and subscriptions. Call it on route changes and on component unmount.
  4. Branch on code: every failure is code + message + retryable. Never parse message.

If the port was changed

After a user changes the port in the console, connected pages receive system.serviceMigrated. Stop issuing new requests and guide the user to the new address rather than retrying against the old one:

client.onServiceMigrated((event) => {
  showReconnectPrompt(event.endpoint);
});

Next steps

On this page