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
- Not installed or not running: probe with
PrintLink.detect()first and guide the user wheninstalled === false, instead of failing fromconnect(). - Waiting for authorisation:
PAIRING_PENDINGis a normal intermediate state. Ask the user to accept on their computer, then retryensureAuthorized(). - Disconnect when leaving:
close()releases the connection and subscriptions. Call it on route changes and on component unmount. - Branch on
code: every failure iscode+message+retryable. Never parsemessage.
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);
});