Skip to content

Printers

Overview

A printer is one type of device managed under Device Management. A printer device represents a network receipt printer that polls Upvendo for work: it is configured with an Upvendo URL carrying its own device token, and it asks the server for jobs on a fixed interval.

The direction is deliberate — Upvendo never dials out to a printer. The only poll protocol shipped today is Epson Server Direct Print: the printer POSTs to POST /api/print/epson/sdp, authenticated by the device token minted when the device was created (routes/api/print.php:39-48). Star CloudPRNT is planned as a sibling route ("Wave 2") and is not shipped (routes/api/print.php:32-36).

There is also a second, newer delivery path — the LAN print bridge, where an on-site POS, Kitchen Display or kiosk claims queued jobs and drives the printer over the local network. See The LAN print bridge.

Printers are not a standalone page. They are created and managed through the same Devices screen as Kiosk and Kitchen Display devices, by choosing the device type "Printer".

Key Purpose: Register a network receipt printer as a device so kitchen-ticket routing can point at it.

Purpose

This area lets you add a printer device: give it a name and a location, and pick the type "Printer". That is the whole create. The server mints the printer's poll token and the device is created already activated, so there is no activation code to type in and no URL shown on the create response.

Key Concepts

  • Printer is a device type: Devices have a type field (DeviceTypes enum) with the values Kiosk, Kitchen Display, POS, and Printer. "Printer" is the value used for receipt printers. There is no separate receipt/kitchen/label printer classification in the device model.
  • Selectable by every merchant: Printer is a live entry in the back office's DATA_TYPE_OPTION_DEVICE list (src/constants.ts:41), listed last on purpose — after POS, Kiosk and Kitchen Display — because a printer is a peripheral rather than a terminal an operator works at. It appears in neither POS_REQUIRED_DEVICE_TYPES nor FIRST_PARTY_POS_REQUIRED_DEVICE_TYPES, so unlike Kiosk / POS / Kitchen Display the tile is enabled for every merchant (SelectDeviceTypeDialog.vue:47-50 is the only path that disables a tile).
  • Adding one is open; provisioning one is not: creating a Printer device is open to every merchant, but everything that turns it into a working printer — GET /devices/unconfigured-printers and the whole /devices/{id}/printer/* group (configure, pairing, test print, rotate, retire) — carries the first-party-pos middleware, which 403s unless the tenant merchant has pos_provider === 'upvendo' (routes/api/backoffice/devices.php:40-41, :135, EnsureFirstPartyPos.php:47-53). That marker is written in exactly one place, FirstPartyPosProvisioningService::selectFirstPartyPos() (:83), but it has two callers: POST /back-office/pos/select-provider (PosProviderController.php:28) and PosDemoSeederService.php:254, which the pos:seed-demo and create:test-account artisan commands drive. Only the first is gated for live merchants: while the provider is staged 'test_only' => true (config/pos-providers.php:52), selectFirstPartyPos() 403s a non-is_test merchant in production (FirstPartyPosProvisioningService.php:70-77). So only test-flagged merchants can pass this gate self-serve — and a merchant seeded from the console holds the marker and reaches the whole provisioning surface, which is why this is staged rather than absent.
  • Create is name + location + type: StoreDeviceRequest adds nothing for a printer (StoreDeviceRequest.php:75-91). mac_address and printer_profile_id were both removed as inputs by Kitchen Routing v2.
  • The create response is {id} only: CreatedDeviceResource returns just the device id for a printer — no activation_code, no URL, and deliberately no token (CreatedDeviceResource.php:30-42). Revealing the URL + token to an operator is a separate, permission-gated provisioning surface.
  • Born activated with a server-minted token: DeviceService::genDevice() mints a 64-character hex token (32 random bytes) and sets is_activated = true for printers, because a printer has no app and therefore no activation-code screen (DeviceService.php:195-209).
  • Poll authentication: the token rides either as ?token=<token> or as the header X-Printer-Token (AuthenticatePrinterToken.php:71, :153-159). The poll route is mounted outside every session/JWT/tenant group — the token itself selects the tenant — behind throttle:printer-poll + printer.auth.
  • Configuration lives in config, not per device: print.printer_base_url (PRINTER_BASE_URL, falling back to app.url) is the origin written into the hardware at pairing, and print.sdp_interval_seconds (PRINTER_SDP_INTERVAL_SECONDS, default 5) is how often the printer asks for work (config/upvendo.php:46-75). The old fleet-wide UPVENDO_PRINTER_URL was removed (config/upvendo.php:41-44).
  • LAN discovery lives in the POS app, not the back office: upvendo-pos ships @upvendo/capacitor-printer-discovery, which browses mDNS/DNS-SD over OS-native APIs to find LAN printers. It is discovery-only — no print, connect or configuration-write methods. Nothing in the back office performs discovery.
  • The MAC and IP arrive from a discovery report, never from a formas a server contract; no shipped client sends one yet. The route is real and enforced, but nothing in upvendo-pos imports @upvendo/capacitor-printer-discovery (it is a package.json dependency only) and upvendo-kiosk has no printer-discovery code at all, so every statement in this bullet describes what the server accepts, not something a merchant's hardware does today. A register or kiosk on the LAN would POST what it found to POST /pos/printers/discovery-report or POST /kiosk/printers/discovery-report (routes/api/pos.php:99-100, routes/api.php:234-235). A report entry may carry only seven keys — mac, ip, model, device_name, serial, paper_width_mm, mac_source (DiscoveryReportRequest.php:54) — and any eighth key fails the whole batch with a 422 naming the accepted list (:72, :106-108). mac is the only required one (:73); paper_width_mm must be 58 or 80 (:90); a batch may carry at most 16 printers (PrinterProvisioningService.php:171). The reports are rate-limited by throttle:printer-discovery — 4/minute and 60/hour per device credential, plus 120/minute per client address (AppServiceProvider.php:291-303).
  • MAC provenance — mac_source: each sighting says how it learned the MAC↔address binding. Three values (PrinterMacSource.php:44-53): ipp-uuid (the _ipp._tcp TXT UUID tail, cross-checked against the mDNS hostname — how an Epson is found), star-http (Star's unauthenticated identity page, reached after a TCP 9100 sweep — how a Star would be found, because a Star advertises nothing on mDNS), and manual (an operator typed the address). Only ipp-uuid has a client behind it. The POS discovery plugin browses mDNS/DNS-SD only, its exported MacSource union is literally 'ipp-uuid' (packages/capacitor-printer-discovery/src/definitions.ts:45) and correlate.ts:210 emits 'ipp-uuid' or nothing — no shipped code produces star-http or manual. Both are values the server accepts, not mechanisms that run. The field is optional on the wire; an omitted value is written as ipp-uuid because that is what every build predating the field actually does (:56-73, PrinterProvisioningService.php:581-584). A row that has no provenance stored reads back as null — the read side never invents one (Device.php:582).
  • Why provenance is tracked: the stored IP used to be inert (a printer polls us, so nothing dialled it). It is now a delivery-control field, so a lower-ranked sighting may not change an address a higher-ranked one established: ipp-uuid and manual rank 2, star-http ranks 1 (PrinterMacSource.php:97-116). It may still confirm an address, and it may still supply one where nothing is recorded — that is how a Star gets its first address. On top of that, a configured printer takes an address change only from a device standing at that printer's own location; an unconfigured one still takes one from any site of the merchant, deliberately (DeviceRepository::refreshPrinterAddress, DeviceRepository.php:483-535).
  • A refused address claim is visible, not silent: when a change is refused, the claim (ip, mac_source, reporting device, reporting location, timestamp) is written onto the printer as network_info.address_conflict and surfaced on the printer resource (DeviceRepository::recordAddressConflict, DeviceRepository.php:576-604; PrinterResource.php:59-67), and logged at warning level as "Printer address change refused" (PrinterProvisioningService.php:600-613). The two readings are both actionable: this printer is homed to the wrong location, or a device at another site is claiming it.
  • No subscription required: unlike Kiosk and Kitchen Display devices, a printer device does not open a Stripe subscription (DeviceService.php:769-775).

Actions

Add a Printer

Create a device of type Printer: provide a name and a location. There is no profile step and no MAC-address step — the naming dialog goes straight to create (NewDeviceDialogs.vue:192-197). On success the back office shows PrinterAddedDialog (src/views/devices/components/dialogs/PrinterAddedDialog.vue), which confirms the printer exists and offers Close plus — only for a first-party-POS merchantManage stations (PrinterAddedDialog.vue:42-43, :103). The hand-off is hidden otherwise, because the Stations tab it leads to is itself first-party-pos-gated. It deliberately shows no poll URL and no token.

The old Printer Activation / Printer Configuration dialog (PrinterActivationDialog.vue, with a URL box, Copy and a stubbed Send button) was deleted. Do not point a merchant at it.

Route the printer to a station

A newly created printer does nothing until a KDS station routes tickets to it — that is the primary action on the post-create dialog, and it is the printer's real configuration in this program. Station routing lives on the Devices page's Stations tab (src/views/devices/components/stations/StationsTab.vue).

Manage Printer Profiles

Printer profiles now have their own back-office page: Device Management → Printer Profiles (/device-management/printer-profiles). It is gated on the device-profile CASL subject — the same VIEW/CREATE/EDIT/DELETE_DEVICE_PROFILE permissions the kiosk and KDS profile families use, with no new permission slug — plus firstPartyPosOnly, because the route group also carries the first-party-pos middleware and the merchant-owner wildcard grants the profile permissions to every merchant (so the permissions alone are not a first-party check).

Full CRUD at routes/api/backoffice/printer-profiles.php:37-53: GET / (list), POST / (create), GET /options, GET /{id}, PUT /{id}, DELETE /{id}.

A location is a precondition, not a filter. The list endpoint requires location_id, so with no location chosen the page shows a hint and never calls the API.

The table lists Name, what it prints, paper width, column mode, assigned device count and last-updated.

What a profile describes

A profile describes a printer — what it prints, its paper, its column mode, its cut. It does not route tickets (that is a station's sinks).

SettingValuesDefault
Prints (prints)kitchen_ticket, receipt — a profile may hold both
Paper width (paper_width_mm)58, 8080
Column mode48/35, 42/3242/32
Cut modeper_ticket (the only mode shipped)per_ticket
  • There is deliberately no fiscal_receipt capability. Whether a customer receipt is a signed VAT receipt is a property of the location's regime, decided at render time — a per-printer choice would be a second source of truth and, at a mandated site, an ungated receipt channel bypassing the fiscal device. One label, every country.
  • 48/35 is the hardware's factory setting; 42/32 is the platform default. The default is the budget that fits under both modes, so an undeclared profile keeps printing exactly what it printed yesterday.
  • Dual-role printers are supported. A profile carrying both capabilities stores role as the array ['kitchen', 'receipt'], so both legacy single-role queries still match it by containment. role is derived at the persistence gate, never caller-supplied — the repository throws if a caller supplies one, so the stored pair can never disagree with prints. A request may still say role: receipt_and_kitchen as a compatibility alias; that word is mapped server-side and never stored.
  • Receipt routing is ambiguous by design when more than one printer can print receipts. The page scans the whole location (not just the visible page) and says so: the paying drawer's kick_binding.printer_device_id wins, otherwise the oldest candidate. Two tills with two drawers is a legitimate layout, so this is an explanation, never a constraint.

Binding a profile to a printer

Assignment is not done from this page, and not through the Devices profile dialog. DeviceService::assignProfile() has no printer branch — it writes only for Kiosk and Kitchen Display — and the back office agrees: deviceTypeHasProfile() returns true only for Kiosk and Kitchen Display (src/utils/deviceProfileFamily.ts), so a printer device has no "Assigned profile" card in its drawer at all.

A printer's profile is bound during pairing/provisioning: PrinterPairingOrchestrator::configure() binds the chosen printer_profile_id when one is supplied, and otherwise derives a default from the paper width the hardware reports at configure time (app/Services/Orchestrators/PrinterPairingOrchestrator.php:169-180).

There is now a merchant-facing test print — the first print path outside the pairing flow and the dev/e2e route (PrinterProvisioningController.php:144-167). It exists on both mounts:

  • back office: POST /back-office/devices/{id}/printer/test-print, permission EDIT_DEVICES, inside the first-party-pos-gated /printer group (routes/api/backoffice/devices.php:135, :147-148);
  • register: POST /pos/printers/{id}/test-print, staff PIN session only (pos.staff) — no manager escalation (routes/api/pos.php:108). Configure and pairing-session demand a manager approval; the test print does not, because it mints nothing and discloses nothing, and it is how an operator tells two identical units on one counter apart (routes/api/pos.php:89-97, PrinterPairingOrchestrator.php:268-284).

Either way a human principal is required — a bare device token is refused, and on the register the device must be a POS one, so a kiosk or KDS token cannot queue a test print (PrinterPairingOrchestrator.php:440-486). Each press enqueues a fresh job (a new idempotency key per call), so pressing twice yields two pieces of paper (:276-284). The printer must already be configured: discovered, pre_registered and retired printers are unroutable and answer 404 (DeviceRepository::retrievePrinterAtLocation, DeviceRepository.php:950-972, filtering on Device::UNROUTABLE_PROVISIONING_STATES; Device.php:225-229).

No back-office button ships for this. Nothing in upvendo-backoffice at the production pin references a test print, and upvendo-pos has no printer screens at all — the whole discover → configure → pair → test-print surface is API-only today.

Rename the Printer

Update the device name via PUT /back-office/devices/{id} (the update request only permits changing name).

Location

  • Backoffice Route: /device-management/devices (route name device-management-devices; nav: "Device Management" > "Devices"). There is no dedicated /devices/printers route — printer devices are managed within the Devices page by device type.
  • Printer Profiles Route: /device-management/printer-profiles (route name device-management-printer-profiles; nav: "Device Management" > "Printer Profiles"). First-party-POS merchants only. Components: src/views/printer-profiles/PrinterProfiles.vue (list) and PrinterProfileForm.vue (editor), page entry src/pages/device-management/printer-profiles/index.vue.
  • Backend Routes: prefixed /back-office/devices, handled by app/Http/Controllers/Api/DeviceController.php (there is no PrinterController). The printer's own poll route is POST /api/print/epson/sdp (routes/api/print.php), and printer provisioning/pairing lives under /back-office/devices/{id}/printer/* and POST /pos/printers/*.
  • Vue Component: src/views/devices/Devices.vue (page entry src/pages/device-management/devices/index.vue). Printer-specific dialogs live under src/views/devices/components/dialogs/PrinterAddedDialog.vue is the post-create screen.

Fields

These are the fields actually accepted when creating a printer device. The model is a MongoDB document (App\RawModels\Device), not a SQL table.

Device Name

PropertyValue
Field IDname
LabelDevice / Printer name
TypeText
RequiredYes
Validationrequired, string, unique per merchant (UniqueInConnectionWithModel)

Description: Identifier for this printer device. (not-verified-here: any max-length limit — none is set in StoreDeviceRequest.)


Device Type

PropertyValue
Field IDtype
LabelDevice type
TypeSelect
OptionsPOS, Kiosk, Kitchen Display, Printer (enum DeviceTypes)
RequiredYes
Validationrequired, enum DeviceTypes

Description: For a printer this is set to Printer. There is no receipt/kitchen/label sub-type.


Assigned Location

PropertyValue
Field IDlocation_id
LabelLocation
TypeSelect
RequiredYes
Validationrequired, string, exists in locations

Description: Which location this device belongs to.


There are no other printer create fields. mac_address and printer_profile_id were deleted from StoreDeviceRequest by Kitchen Routing v2 and are no longer accepted. SelectProfileDialog.vue:112-114 still renders a required MAC field behind deviceForm.type === 'Printer', but printers never reach that dialog — the naming step calls create directly (NewDeviceDialogs.vue:192-197) — so it is still dead UI and must not be documented as a step. A printer's MAC now arrives from a discovery report posted by a register or kiosk on the same LAN, never from a person typing it.

The following fields appeared in earlier documentation but do not exist in the printer device model, request validation, or UI: connection_type, ip_address, port, paper_width, auto_cut, print_logo, copies, category_ids, and an enabled toggle. There is no per-printer category routing and no receipt/kitchen/label type. Connection details such as IP are surfaced read-only via device connectivity, not as editable form fields.

What a printer record reports back (read-only)

PrinterResource is the one shape both the register and the back office read (PrinterResource.php:31-71). It carries id, provisioning_state, location_id, name (null, not "", when nobody has named it yet) and a network_info block:

KeyMeaning
macThe printer's MAC — the identity a report is matched on
mac_sourceHow the sighting that supplied the address knew the printer: ipp-uuid, star-http, manual, or null for a record written before provenance was tracked
ipLast reported address
model, serial, paper_width_mmHardware description; written at birth, and afterwards only while the record is still unconfigured
last_discovered_atISO-8601 timestamp of the last sighting
address_conflictThe last refused address claim (ip, mac_source, reported_by_device_id, location_id, at), or null

The poll token is never in this shape — it appears in exactly one response anywhere, the sdp.url of a pairing session (PrinterProvisioningController.php:28-32, PrinterResource.php:15-19).

How a printer's status reads in the back office

A printer speaks its own status vocabulary, and it is deliberately not the tablet one. seen and unknown are not synonyms for online and offline: nothing polls a printer, so both words say only when Upvendo last looked.

  • seen is intentionally not the same green as online — reachability is not readiness.
  • unknown is intentionally not an alarm colour.
  • The last ticket's outcome outranks reachability. A printer that answers discovery but failed its last ticket reads as broken, not "Seen recently".

The device drawer renders a printer-specific panel (PrinterSection) against ShowDeviceResource.printer / .print_activity, and omits any field it has no value for rather than printing "Unknown". The Connectivity, tablet-schema and device-logs cards are off for printers: a printer carries a disjoint network_info schema on the same field and writes to no device-log collection, so those cards resolved to all-null.

One reading of the status lives in src/utils/deviceStatus.ts and is shared by the fleet-table chip, its colour and the status filter — those three drifting apart was the defect itself.

The device model and back-office code do not define receipt/kitchen/label print-job templates or their contents. Receipt rendering and "when to print" timing for online orders are governed elsewhere — see Print Settings (PrintSettingOptions: pickup_time, when_order_is_placed) and the kiosk/POS receipt flows. (not-verified-here: exact on-paper layout and content, which is determined by the printer profile and device firmware, not by this feature's code.)

The LAN print bridge

There is a second delivery path alongside the printer's own Server Direct Print poll: the LAN print bridge. Whichever on-site screen is awake — a POS register, a Kitchen Display or a kiosk — acts as the site's printing agent, claims queued jobs and puts them on the LAN printer itself.

One bridge per site at a time. A lease is held per (merchant, location): the holder renews it on a ~20-second heartbeat and it retires after a 90-second TTL. Only the holder's claims are honoured. A client that is backgrounded or locked proactively releases the lease, which is the difference between a site that fails over cleanly and one that is silently asleep. The release is compare-and-set on the lease id, so it is a no-op for anyone who is not the holder — otherwise any screen at the site could run a release loop and stop the kitchen printing.

It is device-level, not staff-level, on purpose. Tickets must print when nobody is signed in — a kitchen printer that stops at the end of a shift because the last operator logged out is exactly the silent failure this exists to remove. (A Kitchen Display has no staff-PIN session at all, so a staff gate would exclude the highest-priority bridge class outright.) The compensating controls are all server-side: no lease, no claim; the printer set is derived from the device's own location; and a result is accepted only from the recorded claimant of that exact attempt.

A lapsed KDS stops bridging. The KDS mount additionally enforces an active subscription (402) — a device Upvendo will not serve orders to should not be the site's printing agent either.

First-party POS only, including the kiosk mount. The gate sits once on the shared route file rather than three times at the mount sites, and it resolves the merchant from the bound tenant database, so it works identically under a device JWT.

Routes (routes/api/print-bridge.php, mounted inside the POS, KDS and kiosk device-JWT groups, throttled by throttle:print-bridge keyed on the bearer credential, never the proxy egress address):

MethodPathPurpose
POST/print-bridge/leaseTake or renew the site's lease
DELETE/print-bridge/leaseProactively release it (compare-and-set on lease id)
POST/print-bridge/claimClaim queued jobs for this location
POST/print-bridge/jobs/{id}/resultReport the outcome of one attempt

Business Logic

Device creation (token vs activation code)

Create device (DeviceService::genDevice)

        ├── type = Printer?
        │     └── token = Device::generateSecureToken()   // 64-char hex
        │           is_activated = true                    // born active
        │           API returns { id }                     // nothing else

        └── otherwise (Kiosk / Kitchen Display / POS)
              └── activation_code = Device::generateActivationCode()
                    API returns { id, activation_code }

The poll loop

Printer  ──POST /api/print/epson/sdp?token=<device token>──▶  Upvendo
         ◀──────────── job (or "nothing to print") ──────────

Mounted outside every auth group; throttle:printer-poll then printer.auth resolves the device, binds its merchant's tenant database, and answers every failure with one indistinguishable 404.

No order still produces a print jobPrintJob::SOURCE_ORDER is declared and never emitted (PrintJob.php:102); order-driven ticket printing is Wave 2. But "nothing produces a print job" is no longer true. PrintJobService::enqueueKitchenTicket() has three callers (PrintDevController.php:64, PrinterPairingOrchestrator.php:342), and all three enqueue SOURCE_TEST (PrintJob.php:99):

1. dev/e2e            POST /dev/print/test-ticket        isTestEnv + e2e.auth — 403 in production
2. pairing test ticket  …/pairing-session/confirm-write   the ACK that gates pairing
3. merchant test print  …/printer/test-print              NEW — an operator pressing a button

The test ticket is rendered by the same price-free renderer and sized by the same printer profile a real kitchen ticket will use, so it proves the path that ships (PrinterPairingOrchestrator.php:331-349).

Customer Impact

  • A connected printer is the target a KDS station's routing can point at.
  • No subscription is required to add a printer device (unlike Kiosk/KDS).

Relations

Depends On

  • Locations: Each device is assigned to exactly one location (location_id, required).

Affects

  • Devices: Printers are part of the shared Devices management surface.

Business Rules

  • A printer is a device of type = Printer; the device type enum values are Kiosk, Kitchen Display, POS, Printer. There is no receipt/kitchen/label printer type.
  • The Printer tile is selectable by every merchant — it carries neither the third-party-POS gate nor the first-party-POS gate.
  • Printer devices do not open a Stripe subscription; only Kiosk and Kitchen Display do.
  • A printer create accepts name, location_id and type and nothing else.
  • The create response contains only the device id. The poll token is minted server-side and is not returned.
  • A printer device is created already activated (is_activated = true), so there is no activation code and no code-entry step.
  • Updating a device only allows changing name.
  • Printer-profile CRUD is first-party-POS-only: the whole /back-office/printer-profiles group carries first-party-pos on top of the *_DEVICE_PROFILE permissions.
  • A printer's MAC and IP are only ever written by a discovery report from a device on the same LAN. There is no field anywhere that lets a person type them into the create flow.
  • A discovery-report entry may carry only mac, ip, model, device_name, serial, paper_width_mm, mac_source; any other key rejects the entire batch (422), as does a batch of more than 16 printers.
  • mac_source is optional; an omitted value is stored as ipp-uuid. A record with no stored provenance reads back as null and may be given an address by any source.
  • A star-http sighting may not change an address that an ipp-uuid or manual sighting established. It may confirm one, and it may supply one where none is recorded.
  • Once a printer is configured, only a device at that printer's own location may change its address. While it is still unconfigured, any site of the merchant may.
  • A refused address change is recorded on the printer as address_conflict and logged; it is evidence for an operator and is never used to route a ticket.
  • The merchant-facing test print requires a human principal — a device token alone is refused — and on the register a POS device specifically. The back-office mount needs EDIT_DEVICES; the register mount needs a staff PIN session but no manager escalation.
  • A test print only reaches a configured printer: discovered, pre_registered and retired printers are unroutable and answer 404.
  • Every printer provisioning route on the back office (/devices/{id}/printer/*) and GET /devices/unconfigured-printers carries first-party-pos, so they 403 for any merchant whose pos_provider is not upvendo.

FAQs

  • "What is the difference between a printer and a kiosk/KDS device?" They are all device types. A printer is created already activated with a server-minted poll token and its create response carries only the device id; kiosk and KDS devices return an activation_code you enter on the device, and they require a Stripe subscription. Printers do not.
  • "How do I connect the printer?" Create the printer device, then have the printer's Server Direct Print settings pointed at Upvendo. The URL + token is revealed only through the permission-gated pairing flow — there is no back-office screen that displays it today, so this step is handled during on-site provisioning rather than by copying a URL out of the Devices page.
  • "Do I set an IP address or port for the printer in Upvendo?" No. There are no IP/port/connection-type fields. Connectivity details are shown read-only on the device.
  • "Do I need the printer's MAC address?" No. It used to be required and is not accepted any more — a printer create is name + location + type. The MAC is designed to be learned automatically, from a register or kiosk on the same network reporting the printers it can see. No shipped app sends that report yet, so on a current build nothing populates the MAC at all.
  • "What does 'found by network scan' mean next to my printer?" That is the mac_source on the record, and today you will only ever see ipp-uuid or a blank. ipp-uuid means the printer announced itself over mDNS, which is how an Epson is found — the only method any shipped client implements. star-http (scanning port 9100 and reading a Star's identity page) and manual (somebody typed the address) are accepted by the server but no app produces them yet. The field exists so you can tell an announcement apart from a scan — a scan is easier for the wrong device to imitate.
  • "Why did my printer's IP not update?" Because a weaker sighting cannot move an address a stronger one set, and because once a printer is configured only a device at that printer's own location may change its address. The refused claim is kept on the printer (address_conflict) with which device claimed it, from which site and when. The usual causes are a printer homed to the wrong location, or one flat network shared by two sites.
  • "Can I print a test page?" Yes — that is new. A printer that is already configured can be sent a test ticket from the back office (EDIT_DEVICES) or from the register with just a staff PIN — no manager approval needed. Pressing it twice prints twice. Note that no button ships for it yet in either app; today it is an API-only capability.
  • "Can I route specific menu categories to a kitchen printer?" Not on the printer device — there is no category_ids or category-routing field on a printer. Routing is a KDS station concern, and no order fires a printed ticket yet in any case (Wave 2).

Troubleshooting

Problem: Printer not printing

Possible causes:

  1. The printer's Server Direct Print settings do not point at Upvendo, or carry the wrong token.
  2. Printer offline / not powered.
  3. Paper out.
  4. Nothing was queued: no order produces a print job yet — only test tickets do (pairing, and the merchant test print).

Checks:

  1. Re-run the pairing/provisioning flow so the printer is written with the correct poll URL and token.
  2. Verify the printer's power and network connection.
  3. Refill paper.
  4. Send a test print to confirm the poll loop works end to end.

(not-verified-here: device-side diagnostics and firmware behavior.)

Problem: A test print returns "Printer not found at this location"

Possible causes:

  1. The printer is still discovered or pre_registered — seen on the network but never configured, so it holds no credential and cannot be a print target.
  2. The printer was retired; a retired record keeps its name and location but its credential is revoked.
  3. The printer belongs to a different location than the register asking for the test.

Checks:

  1. Configure and pair the printer first; a discovered record is an invitation to set one up, not a working printer.
  2. If it was retired, the same hardware has to be discovered and configured again.
  3. Confirm the printer's location matches the register's.

Problem: The printer screen shows a refused address claim

Possible causes:

  1. The printer is homed to the wrong location, so the device that can actually see it stands somewhere else.
  2. Two sites share one flat network and a device at the other site is reporting this printer.
  3. A weaker source (a Star identity page) is trying to move an address that mDNS established.

Checks:

  1. Re-home the printer to the location it physically stands at.
  2. Read the conflict record — it names the claiming device, its location and the time.
  3. The corroboration a refused star-http change waits for is a manual sighting — an operator typing the address. No shipped surface produces one today: configure accepts no address field (ConfigurePrinterRequest.php:68-81) and the POS discovery plugin only ever emits ipp-uuid (packages/capacitor-printer-discovery/src/correlate.ts:210). Until that lands, clear a stuck claim by re-homing the printer to the reporting device's location.

Examples

Create a printer device (request)

json
{
  "name": "Receipt Printer - Counter",
  "type": "Printer",
  "location_id": "507f1f77bcf86cd799439014"
}

Create-device response (printer)

json
{
  "id": "507f1f77bcf86cd799439310"
}

(For non-printer device types the response instead contains activation_code.)