Vendra Manual, as shipped in the download
Vendra — User Manual
A self-hosted point-of-sale and inventory system. Version 3.1.4 [src: app/controllers/v3.php:78].
About this manual
Every statement here was written by reading Vendra's own source, and each one carries the file it came from in a bracketed src marker. Paths are relative to the folder this docs directory sits in. If a sentence disagrees with the application, the application is right and this manual has a bug — the markers exist so you can check rather than take our word for it.
Nothing is described that the code does not do. Where something is deliberately limited, that is said plainly rather than left out.
What Vendra is, and what runs it
Vendra is a till and a stock ledger in one application, for a single shop. Money is held as integer cents throughout, so no total is ever the result of floating-point arithmetic [src: app/controllers/v3.php:80].
It runs on PHP and stores its data in either SQLite or MySQL; the driver is chosen in configuration and defaults to SQLite [src: app/src/Database.php:19]. There is no build step and no separate migration command.
Signing in and where you land
Sign in at /login [src: app/controllers/pos.php:43]. After a successful password sign-in you are sent to the application root [src: app/controllers/pos.php:34]. If two-step verification is enrolled you are sent to /login/2fa first [src: app/controllers/pos.php:32].
Single sign-on is available as an alternative route in, and it does one thing the local form does not: it sends a cashier straight to /pos and everyone else to the dashboard [src: app/controllers/v3.php:741].
Users, roles and what each may do
Vendra has four roles: cashier, viewer, manager and admin [src: app/src/Auth.php:67].
Two of those are older than the other two, and the distinction matters when you upgrade. Vendra originally had only admin and cashier [src: app/src/Auth.php:2]. The permission ladder added viewer and manager as an extension rather than a replacement, and existing installations are untouched by it — every stored role is still admin or cashier, both keep exactly the rights they had, and the two new roles exist only once somebody assigns them [src: app/src/Auth.php:53].
The source sets the four out in a comment, and they divide like this: the cashier is the shop floor, with the till, holds and returns but no catalogue, no money reports and no administration; the viewer is a read-only back-office seat, the accountant who sees reports and lists and changes nothing; the manager holds catalogue, purchases, stock, customers and reports, but not users, settings, API keys or restore; and the admin holds everything, including the things that can lock people out or leak credentials [src: app/src/Auth.php:45].
What each role actually holds
The table below was produced by evaluating Vendra's own permission map rather than by reading it, against the seventeen permissions the controllers really enforce [src: app/src/Auth.php:67].
| Permission | cashier | viewer | manager | admin |
|---|---|---|---|---|
pos.use | yes | — | yes | yes |
sale.create | yes | — | yes | yes |
product.view | yes | yes | yes | yes |
catalog.view | — | yes | yes | yes |
product.edit | — | — | yes | yes |
customer.view | yes | yes | yes | yes |
customer.edit | yes | — | yes | yes |
purchase.view | — | yes | yes | yes |
purchase.edit | — | — | yes | yes |
stock.view | — | yes | yes | yes |
stock.verify | — | — | yes | yes |
stock.adjust | — | — | yes | yes |
report.view | — | yes | yes | yes |
audit.view | — | yes | yes | yes |
view.save | — | — | yes | yes |
user.edit | — | — | — | yes |
settings.edit | — | — | — | yes |
Two rows in that table are deliberate and worth understanding, because getting either wrong would loosen access quietly.
A cashier holds product.view but not catalog.view. The till has to be able to look a product up, and so does the reporting tool described later — but the back-office catalogue screen is gated on catalog.view instead, precisely so that the right to look up a price does not hand the shop floor the back office [src: app/src/Auth.php:58].
A cashier also holds customer.view and customer.edit. That is not a new grant: the till has always been able to add a customer mid-sale, and the permission map records the behaviour that already existed rather than introducing it [src: app/src/Auth.php:63]. The customer list is gated on being signed in at all, for the same reason [src: app/controllers/admin.php:324].
Unknown or legacy roles fall through the same map rather than being special-cased [src: app/src/Auth.php:80].
The till
Opening a register
A shift begins by opening a register session with the cash you are starting with [src: app/src/Registers.php:22]. Money paid in or out during the shift — a float top-up, a supplier paid from the drawer — is recorded against the session with a type, an amount and a reason [src: app/src/Registers.php:42].
Ringing a sale
A sale is quoted first and then checked out [src: app/src/Sales.php:22]. Checkout takes the lines, a sale-level discount, the payments, the session, the user, an optional customer and a note [src: app/src/Sales.php:97].
Payments are an array, not a single value, which is the important detail: one sale can be settled across several tenders, so a customer may pay part in cash and the rest by card. The methods carried in the source are cash, card and other [src: app/src/Sales.php:97].
Holding a sale
A sale in progress can be parked and picked up again later, which is what you want when somebody has forgotten their wallet and there is a queue. Holds are created, listed, resumed and deleted from the till, and all four actions require the till permission [src: app/controllers/pos.php:136].
Returns
Items are returned against the original sale, with a method and a reason recorded [src: app/src/Sales.php:176].
Receipts
A receipt is rendered as text from the sale, its items, its payments and your store settings [src: app/src/Sales.php:235].
Closing the register
Closing takes the counted cash and computes over or short as counted minus expected, storing the counted figure, the expected figure and the difference between them [src: app/src/Registers.php:82]. The session summary is available before you close [src: app/src/Registers.php:53].
Closing produces a Z-report [src: app/controllers/pos.php:219]. Access to it follows a rule worth knowing: you may read a Z-report if you hold the reporting permission or if the session was your own — so a cashier can always read their own shift and nobody else's [src: app/controllers/pos.php:219].
Products and stock
Stock cannot be oversold
Every movement goes through one function, and a movement that would take stock below zero is refused rather than clamped [src: app/src/Inventory.php:27].
The refusal is worth describing exactly, because it is stronger than it looks. The decrement is written as a single conditional update — the row is reduced only where the quantity on hand is already at least the amount being taken — and a zero row count is treated as "this would oversell" [src: app/src/Inventory.php:27]. It is one atomic statement rather than a read followed by a write, so two tills selling the last item at the same moment cannot both succeed.
The stock ledger
Every movement is a row: type, quantity change, unit cost, what it refers to, a note and the user who caused it [src: app/src/Inventory.php:27]. Stock is never simply overwritten, so the history of a quantity is recoverable.
Stock can be adjusted by hand for a count correction, damage or shrinkage [src: app/src/Inventory.php:67], verified against a physical count [src: app/src/Inventory.php:86], and valued [src: app/src/Inventory.php:101].
Purchases and suppliers
A purchase is entered as lines of product, quantity and unit cost, and requires the purchase-editing permission [src: app/controllers/admin.php:264].
Three things happen on save, inside one locked transaction so that a failure leaves nothing behind [src: app/controllers/admin.php:264]. A purchase record is created with a generated reference of the form PO- followed by six characters. Each line is written. And each line is received into stock immediately.
That last point is the one to plan around: a purchase is received at the moment you save it. The status is written as received rather than ordered, so there is no pending-order state to reconcile later [src: app/controllers/admin.php:264]. Vendra records what has arrived, not what has been ordered.
How receiving changes cost
Receiving stock updates the product's average cost, weighted by quantity: the existing quantity at the existing average, plus the incoming quantity at the incoming cost, divided by the total [src: app/src/Inventory.php:51].
One detail protects that figure. If on-hand stock is negative, it is treated as zero for the purposes of the average, so a negative quantity cannot poison the cost of everything received afterwards [src: app/src/Inventory.php:51]. The product row is locked while this happens [src: app/src/Inventory.php:51].
Purchases can be listed and opened individually [src: app/controllers/admin.php:251]. Suppliers are managed separately [src: app/controllers/admin.php:302]; deleting one detaches it from its purchases rather than deleting them, so the purchase history survives the supplier [src: app/controllers/admin.php:302].
Reports and the audit log
Reports are gated on the reporting permission [src: app/controllers/admin.php:399]. Actions that change something are written to an audit log, which the viewer role can read [src: app/src/Auth.php:67].
Settings
Settings are administrator-only, both to view and to save [src: app/controllers/admin.php:490].
They fall into two groups. The first covers the business and the receipt: business name, e-mail, phone and address, timezone, currency and currency symbol, the default tax rate and what to call tax, whether negative stock is allowed, the default low-stock threshold, receipt header and footer, accent colour and logo [src: app/controllers/admin.php:495].
The second covers mail and notifications: SMTP host, port, user, password, from-address and security mode, a notification address, and four toggles — low stock, a returned sale, the receipt and the Z-report [src: app/controllers/admin.php:495].
Several defaults are applied on save rather than assumed. An unrecognised timezone falls back to UTC, the SMTP port is clamped to a valid port number, and the security mode must be one of STARTTLS, TLS or none, defaulting to STARTTLS [src: app/controllers/admin.php:495].
Two of those toggles behave differently from the rest by design: the receipt and Z-report toggles are the only two that cause mail to be sent to a customer, so an absent checkbox means off, which is also the migration default. Emailing a customer is always a decision somebody made [src: app/controllers/admin.php:495].
One point of arithmetic worth knowing if you compare the form against the database: the tax rate you type is stored multiplied by one hundred, as basis points [src: app/controllers/admin.php:495].
The API and agent access
Keys
The API authenticates with a key rather than a session. Some endpoints take any valid key, and some require an administrator-scoped one [src: app/src/Api.php:31]. Purchase creation and the report endpoint both require the administrator-scoped key [src: app/controllers/api.php:208].
A machine-readable description of the interface is published [src: app/controllers/api.php:307].
Agent tools
Vendra exposes five tools to an agent [src: app/controllers/v3.php:74]:
| Tool | Writes? | What it does |
|---|---|---|
list_products | no | Lists products with price and current stock |
stock_levels | no | Stock on hand for one product, by id or barcode, or for everything |
low_stock | no | Products at or below their low-stock threshold |
sales_report | no | Sales totals for a date range, by product and by cashier |
record_sale | yes | Records a completed sale and decrements stock |
Four are read-only and one writes, and the tool list says so in its own instructions rather than leaving an agent to infer it [src: app/controllers/v3.php:74]. A read-only key cannot call the one that writes.
The instructions given to an agent also state the oversell rule, because an agent needs to know a call can be refused: record_sale goes through the same checkout path the till uses, so a line exceeding available stock is refused and told how much is left [src: app/controllers/v3.php:74].
Webhooks
Two events can be sent to a URL you control: a recorded sale and a low-stock warning [src: app/src/Webhook.php:16].
Spreadsheets: sales and stock
Sales → Download CSV gives one row per sale line — for the last 30 days, the day you are viewing, or any range with ?from=YYYY-MM-DD&to=YYYY-MM-DD — with quantity, quantity returned, unit price, discount, tax, line total, and the unit and line cost the till captured when it rang the sale [src: app/controllers/admin.php:90], [src: app/src/Exports.php:52]. Products → Stock CSV gives price, weighted-average cost, stock, low-stock level and stock value at cost [src: app/src/Exports.php:87]. The sales file needs the report permission and the stock file the catalogue permission, so a cashier gets neither [src: app/controllers/admin.php:92]. Money is written as plain numbers and text cells are guarded against spreadsheet formulas [src: app/src/Exports.php:32].
Backup and data protection
The whole dataset can be exported as JSON, with fields you nominate redacted, or as a SQLite file [src: app/src/BackupExport.php:19].
For a data-protection request, one subject's data can be exported [src: app/src/Gdpr.php:29] or anonymised in place [src: app/src/Gdpr.php:46].
What Vendra does not do
It does not process card payments. It records how a sale was tendered — cash, card or other — and computes the change; the card itself is taken on whatever terminal you already have [src: app/src/Sales.php:97].
It is a single-shop system. The whole installation carries one settings record, written to the row with id 1 [src: app/controllers/admin.php:495], and there is no notion of a second location, a store identifier or a central head office anywhere in the application.
It has no e-commerce side and does not synchronise stock with a website.