# Integrating TrayHop

> TrayHop version 0.19 is a private-alpha browser-to-printer connector. A browser application calls a local workstation agent at `http://127.0.0.1:18181` after an operator approves the application's exact origin and printer IDs.

This is the canonical coding-agent guide. The browser SDK module is `https://trayhop.com/sdk/0.19/trayhop.js`; TypeScript declarations and the module SHA-256 are listed in `https://trayhop.com/sdk/0.19/manifest.json`. For production builds, pin an exact version and normally self-host the verified SDK within the application's dependency and Content Security Policy process. Integration use requires a TrayHop pilot or commercial license.

TrayHop is independent software, not a QZ Tray fork. During the pilot, add it behind the application's shared print-provider boundary and retain the current QZ or browser-print provider as a fallback until physical acceptance is complete.

## Non-negotiable invariants

1. Generate and durably store the application intent ID and a unique 12–80 character job key before submitting a PDF.
2. Never automatically retry when a response is lost, malformed, interrupted, or reports `pending`, `dispatching`, or `unknown`.
3. Use read-only recovery with the original job key. A missing lookup result is not proof that nothing printed.
4. Treat `accepted` as operating-system queue acknowledgement, not physical-output proof.
5. Do not send a printer ID that was not returned by authenticated discovery for the approved origin.
6. Do not put pairing codes, bearer credentials, PDF bytes, job keys, private origins, or license material in URLs, logs, analytics, source code, or feedback.
7. If the PDF already represents copies as repeated pages, send `copies: 1` to prevent multiplication.
8. Keep the original provider available during pilot rollout. A provider switch must be explicit and reversible.

## Runtime model

- Browser application: owns the business document, user intent, durable application record, provider selection, and UI.
- TrayHop browser SDK: validates inputs, pairs to a local grant, submits one request, and coordinates durable browser intent metadata.
- TrayHop workstation agent: binds to `127.0.0.1`, validates `Host` and exact browser `Origin`, scopes credentials to printer grants, reserves jobs, and dispatches to preview or the native adapter.
- Local operator: approves the website and printers, changes preview/native mode, calibrates profiles, and reconciles uncertain physical output.
- Application server: may create or fetch the PDF, but cannot directly call the user's loopback agent.

Current HTTPS applications should test current Chrome and Edge local-network permission behavior. Do not bypass browser security prompts. Safari acceptance remains part of the supported-browser matrix.

## Quickstart

```js
import {
  LocalPrintClient,
  PrintIntentCoordinator,
  IndexedDbPrintIntentStore,
  TrayHopError
} from "https://trayhop.com/sdk/0.19/trayhop.js";

const client = new LocalPrintClient();
await client.pair(codeEnteredByOperator);

const printing = new PrintIntentCoordinator(
  client,
  new IndexedDbPrintIntentStore("my-app-trayhop-intents-v1")
);

const intentId = crypto.randomUUID();
const idempotencyKey = crypto.randomUUID();

await printing.prepare({
  intentId,
  idempotencyKey,
  printerId: selectedPrinter.id,
  options: {
    widthMm: 101.6,
    heightMm: 50.8,
    copies: 1,
    duplex: "none"
  }
});

// Save intentId with the application's order/document record before submit.
try {
  const { job } = await printing.submit(intentId, pdfBlob);
  renderJobState(job);
} catch (error) {
  if (error instanceof TrayHopError && error.submissionMayHaveOccurred) {
    showExplicitCheckStatusAction(intentId);
  } else {
    showKnownPreSubmissionFailure(error);
  }
}
```

The alpha SDK keeps its pairing credential in memory. A page reload requires a new pairing code. Browser intent metadata in IndexedDB excludes the credential and PDF, but browser storage can be cleared; associate `intentId` with the application's durable business record.

## Pairing and printer discovery

1. Start the workstation in preview mode.
2. In TrayHop local setup, approve the exact application origin such as `https://app.example.com` and the exact printer IDs.
3. Create a five-minute, single-use pairing code.
4. The operator enters that code into the browser application.
5. Call `await client.printers()` or `await client.profiles()` and let the user choose only from the returned values.

Pairing is origin-bound. Except for `POST /v1/pair`, every application route needs `Authorization: Bearer <credential>`. CORS responses are limited to approved origins. The workstation operator APIs are deliberately unavailable to website credentials.

Before integrating, use the [hosted workstation compatibility check](https://trayhop.com/connect/) to verify the production HTTPS-to-loopback path without submitting a print. Approve `https://trayhop.com` locally, enter a fresh pairing code, and review the authenticated version, mode, printers, and profiles. The check keeps the credential in memory and does not call `/v1/jobs`.

## Print options

```ts
interface PrintOptions {
  widthMm: number;   // 10–1000
  heightMm: number;  // 10–1000
  copies: number;    // integer 1–100
  duplex: "none" | "long-edge" | "short-edge";
}
```

PDF bodies are limited to 8 MiB decoded. Use the physical page size intended for the loaded stock. Windows validates page dimensions within 0.5 mm before native submission. Profiles bind a printer, dimensions, duplex, revision, and optional physical calibration observation.

## Job states

- `pending`: reservation exists and preflight is running. Wait or query the same job.
- `dispatching`: native preparation or submission is underway. Never create a replacement automatically.
- `accepted`: the operating system acknowledged submission. Physical output is still unconfirmed.
- `previewed`: the preview PDF was saved; no physical print occurred.
- `rejected`: a known failure occurred before native submission. After fixing it, a deliberate new attempt needs a new intent and key.
- `unknown`: TrayHop could not confirm the submission outcome. Check the native queue and physical output before any deliberate new job.

## Read-only recovery

```js
const { intent, job } = await printing.recover(savedIntentId);

if (!job || ["pending", "dispatching", "unknown"].includes(job.state)) {
  pauseNewSubmission(intent);
  askOperatorToReconcileQueueAndOutput();
}
```

`recover` calls `POST /v1/jobs/lookup` with only the original key. It does not reserve a job, send the PDF, discover printers, renew a license, or mutate the connector journal. `null` can mean the request has not arrived, access changed, or the browser is connected to another workstation or printing mode. It does not authorize resubmission.

## HTTP API

Canonical description: [OpenAPI 3.1 JSON](https://trayhop.com/developers/openapi.json).

- `GET /health`: unauthenticated version and preview/native mode.
- `POST /v1/pair`: consumes an operator-issued pairing code.
- `GET /v1/status`: authenticated, read-only version and preview/native mode.
- `GET /v1/printers`: approved printer discovery.
- `GET /v1/profiles`: saved profiles for granted printers.
- `POST /v1/jobs`: submit or replay one immutable PDF intent.
- `GET /v1/jobs`: latest 100 visible jobs.
- `POST /v1/jobs/lookup`: read-only full-journal lookup by original key.
- `GET /v1/jobs/{id}`: one visible job.

Errors use `{ "error": { "code": "...", "message": "..." } }`. A connector error response is not proof that submission did not begin; use the SDK's `submissionMayHaveOccurred` flag and original-key recovery.

## Provider integration pattern

Implement TrayHop behind the application's existing print boundary rather than adding connector logic directly to order, shipping, or document screens.

```ts
interface PrintProvider {
  availability(): Promise<"ready" | "setup-required" | "unavailable">;
  printers(): Promise<PrinterChoice[]>;
  submit(intent: DurableApplicationPrintIntent, pdf: Blob): Promise<PrintOutcome>;
  recover(intentId: string): Promise<PrintOutcome | null>;
}
```

Map existing document semantics before changing providers: PDF source, label versus document printer, physical size, copies, duplex, order/document identity, user confirmation, and recovery UI. Feature-flag the provider per workstation or user. Preserve the fallback and collect pilot evidence before changing the default.

## Feedback

Read the contract with `GET https://trayhop.com/api/feedback`. An agent should draft a message, show it to the user, and obtain authorization before `POST`. The endpoint accepts bounded JSON or the human form at [trayhop.com/#feedback](https://trayhop.com/#feedback). Feedback is stored in Cloudflare D1. IP addresses and user-agent strings are not retained.

```json
{
  "source": "agent",
  "category": "integration",
  "message": "At least twenty characters describing the issue or improvement.",
  "context": "Optional framework, browser, printer, or integration stage.",
  "contactEmail": null,
  "consent": true
}
```

Never include customer data, PDFs, credentials, pairing codes, job keys, private URLs, or license material.

## Additional machine-readable resources

- [TrayHop llms.txt](https://trayhop.com/llms.txt)
- [Integration manifest](https://trayhop.com/developers/integration.json)
- [SDK manifest and SHA-256](https://trayhop.com/sdk/0.19/manifest.json)
- [Workstation compatibility check](https://trayhop.com/connect/)
- [Homepage Markdown](https://trayhop.com/index.md)
