Invora Manual, as shipped in the download
Invora — User Manual
Invoicing and estimates you own outright. Version 3.2.1 [src: app/controllers/api.php:349].
About this manual
Every statement in this manual was written by reading Invora's own source code, and every one of them 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 here disagrees with the application, the application is right and this manual has a bug — the markers are there so you can check.
Nothing is described that the code does not do. Where a feature is off until you switch it on, or is deliberately limited, that is said plainly rather than omitted.
What Invora is, and what runs it
Invora is a single-tenant invoicing application: one business per installation, with its own database and its own users [src: app/src/Auth.php:2].
It runs on PHP with no Composer dependencies and no build step — the PDF writer, for instance, is written from scratch precisely so that nothing has to be installed alongside it [src: app/src/Pdf.php:2]. Data can live in either SQLite or MySQL; you choose during installation [src: app/install/index.php].
The whole application is served through one front controller, which loads the modules, boots the database, applies any pending schema migrations, and then dispatches your request to a route [src: app/index.php:45].
Schema migrations run on every request and are written to be safe to repeat, so upgrading is a matter of replacing files — there is no separate migration command to remember [src: app/index.php:55].
Installing
Point a web server at the app directory. If config.php is absent, Invora redirects you to the installer rather than showing an error [src: app/index.php:39]. If the file exists but the tables do not, you are sent to the installer as well [src: app/index.php:54].
The installer creates the first administrator [src: app/src/Auth.php:2]. Keep those details safe: see Users, roles and what each may do for why the last remaining administrator cannot be demoted or switched off.
Signing in
Password
Sign in at /login [src: app/index.php:65]. Invora looks up the account by e-mail address, lower-cased and trimmed, and requires the account to be active; a deactivated account cannot sign in at all [src: app/src/Auth.php:9]. On success the session identifier is regenerated, which is the standard protection against session fixation [src: app/src/Auth.php:11].
Repeated failures are rate-limited [src: app/src/LoginRate.php].
Two-step verification
Two-step verification is optional and set up from the /security page [src: app/index.php:105]. It uses standard time-based one-time passwords — SHA1, six digits, a thirty-second period — so any ordinary authenticator application will work [src: app/src/Totp.php:29]. A small amount of clock drift is tolerated [src: app/src/Totp.php:8].
Enrolling produces ten recovery codes, shown once and never again [src: app/src/Totp.php:56]. Only hashes of those codes are stored, which is why they cannot be re-displayed later [src: app/src/Totp.php:12]. Print them or put them in a password manager before you leave the page. A recovery code is consumed when it is used [src: app/src/Totp.php:67].
Once enrolled, signing in adds a second step at /login/2fa [src: app/index.php:103]. You can turn it off again from the same security page [src: app/index.php:108].
Single sign-on
Invora can delegate sign-in to an OpenID Connect provider. Begin at /login/sso, and the provider returns the visitor to /login/sso/callback [src: app/controllers/v3.php:1103]. The provider details — issuer, client identifier, client secret, and an enable switch — are saved by an administrator from the settings page [src: app/views/settings.php].
Finding your way around
The navigation is defined in the shared layout, and the labels there are the ones this manual uses [src: app/views/layout.php:24]. Note in particular that the page for products and tax rates is labelled Items & Tax, even though its address is /catalog [src: app/views/catalog.php].
Each navigation entry carries a single-key access shortcut [src: app/views/layout.php:24].
Clients
Clients are listed at /clients, created at /clients/new, and edited at /clients/{id}/edit [src: app/index.php:86]. Deleting a client is a separate action, open to clerks as well as administrators, and it is refused while the client still has documents [src: app/controllers/app.php:481].
Two data-protection actions sit on each client record and are described under Data-protection tools.
Invoices and estimates
Invora keeps one kind of document with two faces. Invoices are listed at /invoices and estimates at /estimates, and both lists are produced by the same underlying function [src: app/controllers/app.php:211].
Create an invoice at /invoices/new or an estimate at /estimates/new; edit either at /documents/{id}/edit; both save through /documents/save [src: app/index.php:71].
Statuses
A document carries one of these statuses: draft, sent, paid, partial, overdue, accepted, or declined [src: app/controllers/app.php]. The last two belong to estimates. Set a status from the document itself [src: app/index.php:77], or mark many documents as sent at once with the bulk action [src: app/controllers/v3.php:1084].
Turning an estimate into an invoice
An accepted estimate can be converted rather than retyped [src: app/index.php:78].
Discounts
A document-level discount can be expressed either as a percentage or as a fixed amount [src: app/src/Money.php].
Money
Every figure on a document is produced by Invora's money functions, which are tested against a separately verified reference implementation [src: app/tests/run.php:3]. The PDF writer formats those numbers and never recalculates them — a deliberate rule, so that a printed document and the screen can never disagree [src: app/src/Pdf.php:12].
Attachments
Files can be attached to a document, served back from /files/{id}, and deleted [src: app/controllers/v3.php:1078].
Saved views
A filtered list can be saved and later removed [src: app/controllers/v3.php:1082].
Payments
Record a payment against a document at /documents/{id}/payments [src: app/index.php:80]. Removing a payment is restricted to administrators [src: app/controllers/app.php:497].
Recording a payment fires the payment.recorded webhook, and if the payment settles the document, invoice.paid as well [src: app/controllers/app.php:410].
Items & Tax
Reusable line items and tax rates live on the Items & Tax page at /catalog [src: app/views/catalog.php]. Saving and deleting items, and saving and deleting tax rates, are all administrator-only [src: app/index.php:94].
Deleting a tax rate does not delete the items that used it — those items have their tax cleared instead, so nothing silently disappears [src: app/controllers/app.php:525].
Sending a document to a client
The public link
Every document is created with its own unguessable public token [src: app/controllers/app.php:301]. That token gives a client a read-only page at /p/{token}, with printable and PDF versions at /p/{token}/print and /p/{token}/pdf [src: app/index.php:83]. Nobody needs an account to open it.
Sending by e-mail
Client-facing e-mail is off by default and must be switched on by an administrator [src: app/controllers/app.php:567]. Once on, a document can be sent from /documents/{id}/send [src: app/controllers/v3.php:1094], and the message contains the public link described above [src: app/src/ClientMail.php:58].
Sending requires your own SMTP details, entered in settings — Invora does not relay through anybody else's service.
A pay-online link
You can display a link to whatever payment page you already use. It is stored with its scheme checked first, because it is rendered on a page your client opens [src: app/controllers/app.php:564]. Its wording is yours to set and defaults to Pay this invoice online [src: app/controllers/app.php:568].
PDFs
Any document can be downloaded as a PDF from /documents/{id}/pdf [src: app/controllers/v3.php:1106]. Files are A4 and use the core Helvetica fonts rather than embedding anything, which keeps them small [src: app/src/Pdf.php:5].
Recurring invoices
Set a cadence on an invoice at /documents/{id}/recurring [src: app/controllers/v3.php:1095]. The available cadences are a closed set: off, every 7, 14 or 28 days, or every 1, 3 or 12 months [src: app/controllers/v3.php:221]. The fortnightly option exists because that is the shape a good deal of retainer billing actually takes [src: app/controllers/v3.php:224].
Due invoices are generated by calling /cron/recurring from your own scheduler [src: app/controllers/v3.php:1096]. Two things about that call matter:
- It is authenticated by the backup token, passed as
?t=…, and returns HTTP 401 without it [src: app/controllers/v3.php:887]. Mint that token with the Scheduled-backup token action,/settings/backup-token[src: app/controllers/v3.php:1090]. Note where it appears: the action redirects to the Security page, and the token is displayed there once and never again [src: app/controllers/v3.php:697]. - It creates drafts. The response reports how many were drafted, and that call sends nothing to anybody [src: app/controllers/v3.php:893]. (Automatic payment reminders, below, have their own switch and their own route.)
Automatic payment reminders
Until version 3.2, Invora never e-mailed a client without somebody pressing a button. Automatic reminders are the one exception, so they are off until an administrator switches them on [src: app/src/Database.php:158], from their own form on the settings page [src: app/controllers/v3.php:1098]. Nothing is sent unless three things agree: the reminder switch, client e-mail (the master switch described under Sending by e-mail), and your own SMTP details [src: app/src/AutoRemind.php:233].
What you approve
| Setting | Default |
|---|---|
| Steps, in days from the due date (a negative number is before it) | -3, 0, 7, 14 [src: app/src/Database.php:159]; up to six steps, each from -30 to 365 [src: app/src/AutoRemind.php:39] |
| The most reminders sent in one day | 20 [src: app/src/Database.php:160] |
| One subject and message per step | the default wording [src: app/src/AutoRemind.php:95], written into your settings on the first request and never rewritten by a later version [src: app/src/Database.php:230] |
The messages take six placeholders: {client}, {number}, {amount_due} (what is still owed), {due_date}, {pay_link} and {business} [src: app/src/AutoRemind.php:47]. A line holding {pay_link} is left out while you have no payment link [src: app/src/AutoRemind.php:452]. Every reminder ends with the client's own link to the invoice, and a part-paid invoice also states what was received and what remains [src: app/src/AutoRemind.php:460].
Saving with the switch on writes the approval to the audit log (the steps, the cap and a fingerprint of the wording) and opens the preview [src: app/controllers/v3.php:942].
Preview today
/reminders/preview lists exactly which invoices would receive which step today, with the full message each one would get, which invoices are held and why, and when the rest are next due [src: app/controllers/v3.php:953]. Opening it sends nothing: it reads the same plan the sender acts on [src: app/src/AutoRemind.php:390].
Which invoices, and when
- Only invoices that are sent, part-paid or overdue with money still owed; never a draft, an estimate or a paid invoice [src: app/src/AutoRemind.php:246].
- Each step goes to an invoice at most once [src: app/src/AutoRemind.php:53].
- A step keeps trying for up to seven days after its own day, and stops when the next step's day comes [src: app/src/AutoRemind.php:325]. An invoice first seen long after its due date therefore gets one reminder, for the step it is at.
- A step whose day falls on or before the invoice's issue date is skipped [src: app/src/AutoRemind.php:311].
- No reminder goes on the day, or the day after, another mail about the same invoice reached the client [src: app/src/AutoRemind.php:368].
- A paused invoice, or an invoice of a paused client, is skipped [src: app/src/AutoRemind.php:356].
- The invoice is read again just before each send, so a payment recorded a moment earlier stops the reminder [src: app/src/AutoRemind.php:489].
How they go out
The schedule runs whenever somebody opens the dashboard, a few reminders per page load [src: app/controllers/app.php:127], and from /cron/reminders, authenticated by the same backup token as /cron/recurring [src: app/controllers/v3.php:906]. Each attempt first claims its slot, unique per day, invoice and step, so two page loads can never both send the same reminder [src: app/src/ClientMail.php:199]. A send the mail server refuses is recorded as failed in the invoice's send log and tried again on a later day inside the step's window, never twice on one day [src: app/src/AutoRemind.php:365]; a refusal also ends that run [src: app/src/AutoRemind.php:512].
Pausing
Pause one invoice from the invoice's page [src: app/controllers/v3.php:1100], or every invoice of a client from the client's page [src: app/controllers/v3.php:1101]. Clerks can pause and resume as well as administrators, and each change is recorded in the audit log [src: app/controllers/v3.php:968].
What it never does
It never takes a payment, and it never adds a late fee or interest: Invora calculates neither. No API route or AI tool can send, pause or edit a reminder; they can read the schedule at /api/reminders [src: app/index.php:133] and through the reminder_schedule tool [src: app/controllers/v3.php:186]. Loading demo data switches the schedule off, because the sample clients' addresses are not yours to mail [src: app/bin/seed.php:30].
Users, roles and what each may do
Invora has exactly two roles: clerk and admin [src: app/controllers/v3.php:200].
Administrators outrank every other role by design: a check for any named role also passes for an administrator [src: app/src/Auth.php:37]. An unknown or missing role is treated as admin, which means an upgrade never locks an existing user out [src: app/src/Auth.php:38].
The division of work
Of Invora's 104 registered routes, 36 are administrator-only. Broadly, a clerk does the daily work and an administrator owns the installation.
A clerk may create, edit, view, print and send documents; record payments; create and edit clients; upload attachments; save views; use the REST API and the AI endpoint; and manage their own two-step verification.
An administrator additionally holds:
| Area | Administrator-only routes |
|---|---|
| Settings | /settings, /settings/oidc, /settings/backup-token, /settings/ical-token [src: app/controllers/app.php:539], /settings/reminders [src: app/controllers/v3.php:924], /reminders/preview [src: app/controllers/v3.php:955] |
| Users | /users, /users/invite, /users/role, /users/active [src: app/controllers/v3.php:204] |
| Audit | /audit, /audit.csv [src: app/controllers/v3.php:310] |
| Backup and restore | /backup.json, /backup.sqlite, /restore, /restore/commit [src: app/controllers/api.php:365] |
| Import | /import/clients [src: app/controllers/app.php:612] |
| Items and tax | /items/save, /items/{id}/delete, /taxes/save, /taxes/{id}/delete [src: app/controllers/app.php:512] |
| Deletions | /documents/{id}/delete, /payments/{id}/delete [src: app/controllers/app.php:388] |
| API credentials | /api-settings and every key and webhook action beneath it [src: app/controllers/api.php:283] |
| Data protection | /clients/{id}/gdpr.json, /clients/{id}/anonymize [src: app/controllers/v3.php:1075] |
| Demo data | /load-demo, /reset-data [src: app/controllers/app.php:620] |
A clerk who attempts one of these is told they lack permission and returned to the dashboard rather than shown an error page [src: app/src/Auth.php:40].
Inviting somebody
Invite a colleague from /users/invite [src: app/controllers/v3.php:215]. The invitation is a single-use token that expires after seven days [src: app/controllers/v3.php:224], and only its hash is stored — the same discipline Invora applies to API keys [src: app/src/RolesKit.php:50]. If the posted role is not one of the two valid roles, the invitation is created as a clerk [src: app/controllers/v3.php:217].
The recipient accepts at /invite/{token}, choosing their own password [src: app/controllers/v3.php:1068]. Acceptance is checked in constant time and against the expiry [src: app/src/RolesKit.php:63].
The last administrator is protected
Invora will not let you lock everybody out. Demoting the only remaining active administrator is refused [src: app/controllers/v3.php:239], and so is deactivating them [src: app/controllers/v3.php:258].
Settings reference
Settings are administrator-only, at /settings [src: app/controllers/app.php:534]. Every value below is saved from that one form [src: app/controllers/app.php:537].
Your business
| Setting | Notes |
|---|---|
| Business name | Defaults to My Business; stored up to 190 characters [src: app/controllers/app.php:546] |
| E-mail, phone, address, website | Phone is limited to 60 characters, the others to 190 [src: app/controllers/app.php:547] |
| Logo | Either a URL in the logo field, or an uploaded file. The upload wins over the URL [src: app/controllers/app.php:575] |
| Accent colour | Defaults to #4f46e5 [src: app/controllers/app.php:550] |
| Time zone | Validated against the system's own zone list; anything unrecognised falls back to UTC [src: app/controllers/app.php:540] |
An uploaded logo is checked by its magic bytes rather than its file name, and replacing one deletes the old file [src: app/controllers/app.php:575]. Logo changes are written to the audit log [src: app/controllers/app.php:579].
Documents
| Setting | Default |
|---|---|
| Currency code | USD [src: app/controllers/app.php:551] |
| Currency symbol | $ [src: app/controllers/app.php:551] |
| Invoice number prefix | INV- [src: app/controllers/app.php:552] |
| Estimate number prefix | EST- [src: app/controllers/app.php:552] |
| Default terms, footer note | Free text, no length limit applied [src: app/controllers/app.php:553] |
Outgoing e-mail
Host, port, username, sender address and encryption are all set here [src: app/controllers/app.php:556]. Two details are worth knowing:
- The port is clamped to a valid range and defaults to 587 [src: app/controllers/app.php:557].
- Encryption must be one of
starttls,sslornone, and anything else becomesstarttls[src: app/controllers/app.php:559]. - The password is only overwritten when you actually type one, so re-saving the form does not wipe a stored password [src: app/controllers/app.php:562].
Notifications
Notifications to you — invoice paid, estimate accepted — are separate from notifications to your client [src: app/controllers/app.php:560]. The client-facing ones are all off unless switched on [src: app/controllers/app.php:566]. Automatic payment reminders have their own form on the same page, described under Automatic payment reminders.
Backup, restore and the scheduled backup
Taking a backup
Two formats, both administrator-only. /backup.json writes every user table as pretty-printed JSON, stamped with the application name, version and an export timestamp [src: app/src/BackupExport.php:19]. /backup.sqlite hands you the raw database file [src: app/src/BackupExport.php:46]. Internal SQLite bookkeeping tables are excluded from the JSON [src: app/src/BackupExport.php:59]. The JSON is the one you can hand on: it and the scheduled backup leave out the same list of credentials — password and API-key hashes, webhook and single sign-on secrets, the SMTP password, two-factor seeds and recovery codes, and the hashes of the backup and calendar tokens [src: app/controllers/v3.php:620].
Restoring
Restoring is deliberately two steps. Upload the file at /restore, then confirm at /restore/commit [src: app/controllers/v3.php:1087]. The intermediate step is what lets you see what you are about to overwrite before you overwrite it. Restoring and minting the backup token are an administrator's; a signed-in clerk gets a 403 [src: app/controllers/v3.php:625].
Unattended backups
Mint a backup token with /settings/backup-token [src: app/controllers/v3.php:1090], then have your scheduler fetch /backup/scheduled [src: app/controllers/v3.php:1089].
Three details to get right. The token is prefixed ibk_ [src: app/controllers/v3.php:691]. It is shown once, on the Security page rather than the settings page you minted it from [src: app/controllers/v3.php:697]. And the same token authenticates the recurring-invoice run, so one secret covers both scheduled jobs [src: app/controllers/v3.php:887].
Importing clients from a CSV file
Administrators import at /import/clients [src: app/controllers/app.php:612]. Columns are matched by header name, not by position, and the matching ignores case, spaces and underscores while accepting a few common aliases [src: app/src/CsvImport.php:5]. The client name is the only required column; e-mail and the rest are optional [src: app/src/CsvImport.php:11].
The REST API
Manage keys at /api-settings [src: app/index.php:115]. Keys carry the prefix apk_ and are shown once — only a hash is kept [src: app/src/Api.php:83].
Two scopes
A key is either read or full, and anything that is not exactly read is stored as full [src: app/src/Api.php:86]. A read-only key is refused at the door on any write, rather than part-way through [src: app/src/Api.php:42].
Endpoints
/api/ping, /api/invoices, /api/estimates, /api/documents/{id}, /api/clients, /api/items and /api/taxes read; invoices, estimates, clients, payments and document statuses can be written [src: app/index.php:121]. A machine-readable description is served from /api/openapi.json [src: app/index.php:134].
API requests authenticate with a bearer key and are exempt from cross-site request forgery checks by design, because they are not browser form posts [src: app/index.php:114].
Webhooks
Create and manage webhooks alongside API keys [src: app/index.php:118]. Invora fires five events:
| Event | When |
|---|---|
invoice.created | a new invoice is created: in the editor [src: app/controllers/app.php:313], by converting an estimate [src: app/controllers/app.php:379], through the API or an AI tool [src: app/controllers/api.php:94], or by a recurring schedule [src: app/controllers/v3.php:819] |
estimate.created | a new estimate is created, in the editor, through the API or an AI tool [src: app/controllers/app.php:313] |
invoice.paid | a payment settles an invoice [src: app/controllers/app.php:412] |
payment.recorded | any payment is recorded [src: app/controllers/app.php:410] |
estimate.accepted | an estimate is marked accepted [src: app/controllers/app.php:350], or converted to an invoice [src: app/controllers/app.php:381] |
Each fires from both the web interface and the API, so an integration sees the same events either way [src: app/controllers/api.php:123]. A webhook can be paused without deleting it [src: app/controllers/api.php:331], and deleting one also clears its delivery history [src: app/controllers/api.php:338].
Connecting an AI assistant
Invora speaks the Model Context Protocol at /mcp [src: app/controllers/v3.php:1062]. Sending a GET returns 405 with an Allow: POST header rather than a confusing error [src: app/controllers/v3.php:30].
Ten tools are exposed: list_invoices, list_estimates, get_document, create_invoice, create_estimate, record_payment, set_status, overdue_report, list_clients [src: app/controllers/v3.php:56] and reminder_schedule, which reads the automatic payment-reminder schedule [src: app/controllers/v3.php:186]. No tool e-mails a customer, sends or pauses a reminder, or deletes anything.
The API key's scope is handed to the protocol core, so a read-only key gets read-only tools — the restriction is enforced per tool, not merely advertised [src: app/controllers/v3.php:37].
The calendar feed
Invoice dates can be subscribed to as a calendar. Mint a calendar token with /settings/ical-token [src: app/controllers/v3.php:1092] — like the backup token it is shown once, on the Security page [src: app/controllers/v3.php:615] — then point your calendar application at /calendar.ics?t=… [src: app/controllers/v3.php:1091]. Without a valid token the feed returns 401 [src: app/controllers/v3.php:586]. The feed covers invoices only, most recent first, up to 500 of them [src: app/controllers/v3.php:591].
Data-protection tools
Two administrator actions sit on each client. /clients/{id}/gdpr.json exports everything held about that client [src: app/controllers/v3.php:1075], and /clients/{id}/anonymize anonymises the record in place [src: app/controllers/v3.php:1076].
Anonymising is not the same as deleting: it is the option to reach for when a client asks to be forgotten but the documents must stay for your own accounting.
The audit log
Administrators can read the log at /audit and export it at /audit.csv [src: app/controllers/v3.php:1071]. Recorded actions include settings saves, status changes, payments, recurring-invoice runs, role changes, invitations and their acceptance, and logo changes [src: app/controllers/v3.php:226], as well as the reminder schedule's approval [src: app/controllers/v3.php:942], its pauses [src: app/controllers/v3.php:968] and every automatic reminder the mail server accepted or refused [src: app/src/AutoRemind.php:509].
Health check
/healthz returns {"ok":true,"app":"invora","version":"3.2.1"} [src: app/controllers/api.php:349]. It needs no administrator rights, which makes it usable by an uptime monitor.
Two things that are not for you to switch on
Both are mentioned because you may see them referred to elsewhere.
Public-demo mode. If a marker file named .demo-mode is present, Invora signs any visitor in as the first user so that a public demonstration needs no password, and it periodically resets itself [src: app/src/Auth.php:24]. That marker is never included in a shipped copy [src: app/src/Auth.php:23]. Your installation does not behave this way.
Hosted operation. Invora can boot against a database handed to it by a control plane instead of its own config.php, which is how a hosted edition works. A standalone installation never sets that value and behaves exactly as this manual describes [src: app/index.php:33].
Where things live
| Path | Contents |
|---|---|
app/index.php | the front controller and most route registrations [src: app/index.php:64] |
app/controllers/ | the request handlers, in three files [src: app/index.php:60] |
app/src/ | the modules — money, PDF, mail, API, roles, backup and the rest [src: app/index.php:7] |
app/views/ | the pages [src: app/views/layout.php] |
app/tests/run.php | the test suite [src: app/tests/run.php:3] |
app/API.md | the API reference [src: app/API.md] |