{
  "openapi": "3.1.0",
  "info": {
    "title": "TrayHop local printing API",
    "version": "0.19.0-alpha.1",
    "description": "Browser-to-loopback API for approved local PDF printing. Pairing is bound to the browser's exact Origin. An accepted job only confirms operating-system queue acknowledgement. Never automatically retry an uncertain submission."
  },
  "servers": [{ "url": "http://127.0.0.1:18181", "description": "TrayHop workstation agent" }],
  "externalDocs": { "description": "TrayHop integration guide", "url": "https://trayhop.com/developers/" },
  "tags": [
    { "name": "Status" },
    { "name": "Pairing" },
    { "name": "Printers" },
    { "name": "Profiles" },
    { "name": "Jobs" }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": ["Status"],
        "summary": "Check whether the local agent is running",
        "security": [],
        "responses": {
          "200": { "description": "Agent status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } } }
        }
      }
    },
    "/v1/pair": {
      "post": {
        "tags": ["Pairing"],
        "summary": "Exchange a local, single-use pairing code for a website credential",
        "description": "The workstation operator must first approve the request Origin and exact printer IDs. The code expires after five minutes and is consumed on success.",
        "security": [],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PairRequest" } } } },
        "responses": {
          "201": { "description": "Paired", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": ["token"], "properties": { "token": { "type": "string", "description": "Keep in memory; never place in a URL or log." } } } } } },
          "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v1/status": {
      "get": {
        "tags": ["Status"],
        "summary": "Read the authenticated agent version and printing mode",
        "description": "Read only. Does not discover printers, submit a document, renew a license, or mutate workstation state.",
        "security": [{ "websiteToken": [] }],
        "responses": {
          "200": { "description": "Authenticated agent status", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": ["status"], "properties": { "status": { "$ref": "#/components/schemas/AgentStatus" } } } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v1/printers": {
      "get": {
        "tags": ["Printers"], "summary": "List printers approved for this website", "security": [{ "websiteToken": [] }],
        "responses": { "200": { "description": "Approved printers", "content": { "application/json": { "schema": { "type": "object", "required": ["printers"], "properties": { "printers": { "type": "array", "items": { "$ref": "#/components/schemas/Printer" } } } } } } }, "401": { "$ref": "#/components/responses/Error" }, "403": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/profiles": {
      "get": {
        "tags": ["Profiles"], "summary": "List operator-managed profiles for approved printers", "security": [{ "websiteToken": [] }],
        "responses": { "200": { "description": "Approved profiles", "content": { "application/json": { "schema": { "type": "object", "required": ["profiles"], "properties": { "profiles": { "type": "array", "items": { "$ref": "#/components/schemas/PublicProfile" } } } } } } }, "401": { "$ref": "#/components/responses/Error" }, "403": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/v1/jobs": {
      "get": {
        "tags": ["Jobs"], "summary": "List recent jobs visible to this website", "security": [{ "websiteToken": [] }],
        "responses": { "200": { "description": "Recent jobs", "content": { "application/json": { "schema": { "type": "object", "required": ["jobs"], "properties": { "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/PublicJob" } } } } } } }, "401": { "$ref": "#/components/responses/Error" }, "403": { "$ref": "#/components/responses/Error" } }
      },
      "post": {
        "tags": ["Jobs"],
        "summary": "Submit one immutable PDF print intent",
        "description": "Persist the intent and idempotency key before this call. A connection error or malformed response may occur after native submission began; reconcile with /v1/jobs/lookup and never automatically resubmit.",
        "security": [{ "websiteToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PrintJobRequest" } } } },
        "responses": {
          "200": { "description": "Current state for the submitted or previously recorded intent", "content": { "application/json": { "schema": { "type": "object", "required": ["job"], "properties": { "job": { "$ref": "#/components/schemas/PublicJob" } } } } } },
          "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" }, "403": { "$ref": "#/components/responses/Error" }, "409": { "$ref": "#/components/responses/Error" }, "413": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v1/jobs/lookup": {
      "post": {
        "tags": ["Jobs"],
        "summary": "Recover a retained job with its original key",
        "description": "Read only. A null result is not proof that nothing printed and does not authorize a retry.",
        "security": [{ "websiteToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LookupRequest" } } } },
        "responses": {
          "200": { "description": "Matching job or null", "content": { "application/json": { "schema": { "type": "object", "required": ["job"], "properties": { "job": { "oneOf": [{ "$ref": "#/components/schemas/PublicJob" }, { "type": "null" }] } } } } } },
          "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" }, "403": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": ["Jobs"], "summary": "Read one visible job", "security": [{ "websiteToken": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Uuid" } }],
        "responses": { "200": { "description": "Job", "content": { "application/json": { "schema": { "type": "object", "required": ["job"], "properties": { "job": { "$ref": "#/components/schemas/PublicJob" } } } } } }, "401": { "$ref": "#/components/responses/Error" }, "403": { "$ref": "#/components/responses/Error" }, "404": { "$ref": "#/components/responses/Error" } }
      }
    }
  },
  "components": {
    "securitySchemes": { "websiteToken": { "type": "http", "scheme": "bearer", "description": "Origin-scoped credential returned by pairing. Keep it in memory." } },
    "responses": { "Error": { "description": "Bounded connector error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } },
    "schemas": {
      "Uuid": { "type": "string", "format": "uuid", "pattern": "^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$" },
      "JobKey": { "type": "string", "minLength": 12, "maxLength": 80, "pattern": "^[A-Za-z0-9_-]{12,80}$", "description": "Create and persist a unique key before submission." },
      "Health": { "type": "object", "additionalProperties": true, "description": "Public liveness metadata; never grants printing access." },
      "AgentStatus": { "type": "object", "additionalProperties": false, "required": ["version", "mode"], "properties": { "version": { "type": "string", "maxLength": 40 }, "mode": { "type": "string", "enum": ["preview", "native"] } } },
      "PairRequest": { "type": "object", "additionalProperties": false, "required": ["code"], "properties": { "code": { "type": "string", "minLength": 1, "maxLength": 100 } } },
      "Printer": {
        "type": "object", "additionalProperties": false, "required": ["id", "name", "backend", "canPrintPdf"],
        "properties": { "id": { "type": "string", "maxLength": 200 }, "name": { "type": "string" }, "backend": { "type": "string", "enum": ["preview", "cups", "windows"] }, "canPrintPdf": { "type": "boolean" } }
      },
      "PrintOptions": {
        "type": "object", "additionalProperties": false, "required": ["widthMm", "heightMm", "copies", "duplex"],
        "properties": { "widthMm": { "type": "number", "minimum": 10, "maximum": 1000 }, "heightMm": { "type": "number", "minimum": 10, "maximum": 1000 }, "copies": { "type": "integer", "minimum": 1, "maximum": 100 }, "duplex": { "type": "string", "enum": ["none", "long-edge", "short-edge"] } }
      },
      "ProfileIdentity": { "type": "object", "additionalProperties": false, "required": ["id", "revision"], "properties": { "id": { "$ref": "#/components/schemas/Uuid" }, "revision": { "type": "integer", "minimum": 1 } } },
      "PublicProfile": {
        "type": "object", "required": ["id", "revision", "name", "kind", "printerId", "paper", "updatedAt"],
        "properties": { "id": { "$ref": "#/components/schemas/Uuid" }, "revision": { "type": "integer", "minimum": 1 }, "name": { "type": "string" }, "kind": { "type": "string", "enum": ["label", "document"] }, "printerId": { "type": "string" }, "paper": { "type": "object", "required": ["widthMm", "heightMm", "duplex"], "properties": { "widthMm": { "type": "number" }, "heightMm": { "type": "number" }, "duplex": { "type": "string", "enum": ["none", "long-edge", "short-edge"] } } }, "updatedAt": { "type": "string", "format": "date-time" }, "calibration": { "type": "object", "required": ["checkedAt"], "properties": { "checkedAt": { "type": "string", "format": "date-time" } } } }
      },
      "PrintJobRequest": {
        "type": "object", "additionalProperties": false, "required": ["idempotencyKey", "printerId", "pdfBase64", "options"],
        "properties": { "idempotencyKey": { "$ref": "#/components/schemas/JobKey" }, "printerId": { "type": "string", "minLength": 1, "maxLength": 200 }, "pdfBase64": { "type": "string", "contentEncoding": "base64", "description": "Base64 encoded PDF, at most 8 MiB decoded." }, "options": { "$ref": "#/components/schemas/PrintOptions" }, "profile": { "$ref": "#/components/schemas/ProfileIdentity" } }
      },
      "LookupRequest": { "type": "object", "additionalProperties": false, "required": ["idempotencyKey"], "properties": { "idempotencyKey": { "$ref": "#/components/schemas/JobKey" } } },
      "PublicJob": {
        "type": "object", "required": ["id", "idempotencyKey", "printerId", "state", "createdAt", "updatedAt", "message"],
        "properties": { "id": { "$ref": "#/components/schemas/Uuid" }, "idempotencyKey": { "$ref": "#/components/schemas/JobKey" }, "printerId": { "type": "string" }, "state": { "type": "string", "enum": ["pending", "dispatching", "accepted", "previewed", "rejected", "unknown"] }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "message": { "type": "string" }, "spoolerId": { "type": "string" }, "profile": { "$ref": "#/components/schemas/ProfileIdentity" }, "options": { "$ref": "#/components/schemas/PrintOptions" }, "review": { "type": "object", "properties": { "revision": { "type": "integer" }, "outcome": { "type": "string", "enum": ["printed", "not-printed", "partial"] }, "reviewedAt": { "type": "string", "format": "date-time" }, "queueChecked": { "const": true }, "outputChecked": { "const": true } } } }
      },
      "ErrorResponse": { "type": "object", "required": ["error"], "properties": { "error": { "type": "object", "required": ["code", "message"], "properties": { "code": { "type": "string" }, "message": { "type": "string" } } } } }
    }
  },
  "x-trayhop-invariants": [
    "The browser, not a hosted server, calls the workstation loopback API.",
    "Persist the immutable intent and job key before submission.",
    "Never automatically retry an uncertain submission.",
    "Accepted is not physical-output proof."
  ]
}
