Home›Consigna›Manual
Consigna · Manual

Consigna Manual

Consigna — User Manual

A consignment shop's items, sales, splits and payouts. Version 4.1.3 [src: app/controllers/api.php:129].

About this manual

Every statement here was written by reading Consigna'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 Consigna is

People leave goods, the shop sells them and keeps a share [src: app/controllers/mcp.php:55]. The engine that divides the money is what the source calls the moat: money correctness is everything [src: app/src/Consign.php:3].

The money, and why it cannot drift

All money is integer cents. All splits are integer basis points, where 10,000 is 100%. Floats never touch a stored or computed amount [src: app/src/Consign.php:5].

The consignor's share is computed as intdiv(sale × bp + 5000, 10000) — round half up, integers only — and the shop's share is the remainder [src: app/src/Consign.php:42].

That last detail is the whole trick. Because the shop gets what is left rather than a second rounded calculation, the two shares always sum exactly to the sale price. The source states the consequence: every rounding penny lands in exactly one pocket, never lost, never duplicated [src: app/src/Consign.php:10].

A product that rounded both halves separately would, on odd amounts, either invent a penny or lose one. This one cannot.

The split is snapshotted onto the sale

The basis points and both shares are written onto the sale row at the moment of sale [src: app/src/Consign.php:203], and the source is explicit about why: later split changes never rewrite history [src: app/src/Consign.php:14].

Renegotiate a consignor's terms and past sales keep the terms they were made under.

Which split applies

An item's own override where one is set, otherwise the consignor's default [src: app/src/Consign.php:49], clamped to the 0–10,000 range.

Selling an item

Only an available item can be sold, and the refusal names the actual state [src: app/src/Consign.php:173] — already sold, returned to the consignor, donated, or expired, with expired telling you to set it back to available first.

The sale is atomic. The flip from available to sold is a conditional update inside a transaction, so if the update matches no rows another request got there first and the answer is a refusal rather than a second sale [src: app/src/Consign.php:193].

A negative sale price is refused [src: app/src/Consign.php:182], as is an invalid date.

The counter: several items, one receipt

Since 3.2.0, Counter rings up a basket. Scan a tag — a USB or Bluetooth scanner types its SKU and presses Enter — or type a SKU or a few words of the title, and the item joins the basket; only items on the floor can [src: app/controllers/counter.php:24]. The server prices the basket on every change, so what the screen shows is what will be recorded [src: app/controllers/counter.php:62]. You can change a line's price before completing (a haggle, a bundle price).

All or nothing. Every item flips from available to sold inside one transaction, with the same conditional update a single sale uses; if any item was sold a moment ago by someone else, the whole basket is refused and nothing is written [src: app/src/Consign.php:352]. Each item still gets its own sale row, split with its own share and snapshotted, so statements, the portal and the consignor emails work exactly as for a single sale.

Tax. One shop-wide rate, set in Settings: none, added on top of the tag price, or included in it. Tax is worked out per item, rounded half up in whole cents, and the consignor's share is always calculated on the price without tax — the tax belongs to neither of you [src: app/src/Consign.php:293]. The receipt shows the rate and the tax.

Payment. Choose cash, card (on your own terminal) or other. For cash, enter what was handed over; the sale is refused if it is less than the total, and the change is recorded [src: app/src/Consign.php:352]. Consigna takes no card payment itself.

The receipt opens ready to print on A4 or an 80 mm roll, with your receipt footer [src: app/controllers/counter.php:94]. Sales lists each item's line with a link to its receipt.

Voiding. An administrator can void a counter sale from its receipt: every item goes back on the floor and its sale rows are removed, in one transaction — refused when a consignor has already been paid out against it [src: app/src/Consign.php:405]. A single line of a counter sale cannot be undone on its own, because the customer holds a receipt with that total; the refusal names the receipt [src: app/src/Consign.php:227].

Price tags

Items → Print tags lists the items on the floor not yet tagged (or every item, or one consignor's, or those taken in since a date). Choose the label stock — an A4 sheet of 24 (70 × 37 mm), a US Letter sheet of 30 (2⅝ × 1 in) or a 50 × 30 mm label-printer roll — and how many labels are already used on a part-used sheet [src: app/controllers/counter.php:123]. Each tag carries the title, the price, the SKU as a Code 128 barcode with the SKU printed beneath it, the consignor's number (C12, never their name) and the intake date [src: app/views/tags_sheet.php:36]. Print at actual size. Mark as tagged keeps those items out of the "not yet tagged" list [src: app/controllers/counter.php:176]. A SKU with characters a barcode cannot carry (an accent, a tab) prints a note instead of a barcode.

Undoing a sale

An undo deletes the sale and puts the item back on the floor, in one transaction [src: app/src/Consign.php:235].

It is refused when the consignor has already been paid against it — specifically, when removing that sale would take their balance below zero [src: app/src/Consign.php:232].

That is the guard that keeps the ledger honest. Without it, undoing a sale after paying somebody would leave the shop having paid out money the books no longer say was owed.

Payouts

A payout can never exceed the consignor's current balance [src: app/src/Consign.php:248], and the refusal tells you what is actually available [src: app/src/Consign.php:265]. A zero or negative amount is refused [src: app/src/Consign.php:257].

The balance is the sum of consignor shares on their sold items minus the sum of their payouts [src: app/src/Consign.php:16] — derived, not stored, so it cannot disagree with the sales and payouts it is made of.

The check happens inside the transaction that writes the payout [src: app/src/Consign.php:260], so two payouts racing each other cannot both pass a balance check that only one of them should.

The bank file (3.2.0)

Payouts → Bank file writes one file for your bank with the payouts recorded on a day, so you upload it to your online banking instead of typing each transfer. Three formats: US ACH (a NACHA file: one batch of credits, checking or savings) [src: app/src/BankFile.php:65], SEPA credit transfer in euro (ISO 20022 pain.001.001.03) [src: app/src/BankFile.php:94], and a bulk-payment CSV with name, account, bank code, amount and reference for other banks' import templates [src: app/src/BankFile.php:122]. Consigna still moves no money: the file is instructions your bank carries out when you approve it.

A consignor's bank details go on their edit page. They are checked before they are saved: an IBAN by its check digits [src: app/src/BankFile.php:27], a US routing number by its checksum [src: app/src/BankFile.php:43]. Screens show only the last four characters. Your own side (company name, routing number and ACH company ID, or IBAN) goes under Your bank on the same page.

Each payout is marked with the file it went into, so downloading again leaves it out unless you tick "include payouts already in a file" [src: app/controllers/bankfile.php:113]. A payout whose consignor has no details for the chosen format is listed apart, to pay another way [src: app/controllers/bankfile.php:35]. A date in the past goes into the file as the earliest date a bank accepts: tomorrow for ACH, today for SEPA.

Item lifecycle

available --sale--> sold --undo--> available
available <--set-status--> returned / donated / expired

[src: app/src/Consign.php:19]

Five statuses [src: app/src/Consign.php:26], of which four can be set directly — sold is reachable only by recording a sale [src: app/src/Consign.php:28], and a sold item's status is locked until the sale is undone [src: app/src/Consign.php:446].

The consignor portal

A consignor can be issued a token link and have it revoked [src: app/index.php:92]. The token is the credential on a public route [src: app/index.php:167], and the consignor can pull their own statement as a PDF [src: app/index.php:166].

Since 3.2.0 the link can also be emailed from the consignor's page: Consigna issues a new link and sends it to the consignor through your mail server, and never shows it to the shop or stores it (only its hash); the earlier link stops working only when the mail server accepts the new one [src: app/controllers/notices.php:44].

Telling consignors by email

Two switches under Settings → Write to the consignor, both off on a fresh install and both sent only through your own SMTP server, to consignors with an email address on file [src: app/controllers/api.php:37]:

  • when one of their items sells — the item, the date, the price, their share and their balance with you [src: app/controllers/api.php:82];
  • when a payout to them is recorded — the amount, the date, the method and their remaining balance [src: app/controllers/api.php:114].

Two more since 3.2.0, also off until switched on:

  • a reminder to collect items before their expiry date — every item still on the floor whose expiry falls within the days you choose, with the collect-by date and your own line about what happens to uncollected items [src: app/src/Notices.php:41];
  • once a week, what of theirs sold — only when something did in the last seven days, with their share and balance [src: app/src/Notices.php:41].

These two go out from Messages (which lists what is due before you send it) or from a scheduled job running php bin/notices.php. Each goes once: a reminder once per item and expiry date (move the date and the item is announced again), the weekly note once per consignor per week. A message is recorded as sent only when the mail server accepts it, so one that fails is tried again next time [src: app/src/Notices.php:111]. A consignor can be marked No automatic emails on their edit page; that silences all four messages [src: app/controllers/api.php:43].

A mail server that refuses the message never undoes the sale or the payout. A sale recorded by an AI agent sends the same notes and fires the same webhook as one recorded on screen or through the API.

Users and roles

Three roles [src: app/controllers/v3.php:27]. The source names the two things that stay with the owner and gives one reason covering both — they are the consignor's contract and the consignor's money [src: app/controllers/v3.php:20]:

  • payout.record, because a payout is cash leaving the till against someone else's balance [src: app/controllers/v3.php:22].
  • consignor.edit, because the split is the agreement, and changing it silently re-prices every future sale for that person [src: app/controllers/v3.php:23].

Deleting is admin-only for the same kind of reason: a sale row is what a settlement is computed from [src: app/controllers/v3.php:25].

Permissionviewermemberadmin
.view permissionsyesyesyes
audit.viewyesyesyes
item.edit—yesyes
sale.record—yesyes
portal.issue—yesyes
payout.record——yes
consignor.edit——yes
deletion, settings, team——yes

The shop floor takes items in, puts them out and records a sale [src: app/controllers/v3.php:19]. It does not pay anybody or change anybody's terms.

The audit trail is not the viewer's

The viewer's grants are listed one by one — consignors, items, payouts and sales [src: app/controllers/v3.php:32] — so the audit trail is not among them. Before 3.1.4 the viewer held the wildcard *.view, which matched audit.view as well.

An API key carries its holder's role at the REST door as it does at the agent door: adding an item needs item.edit, recording a sale sale.record, and recording a payout payout.record, which only an administrator holds [src: app/controllers/api.php:226]. Before 3.1.4 the REST writes checked only the key's read/write scope.

Backups follow the model

Consigna's database backup downloads are permission-gated, requiring the settings permission [src: app/controllers/api.php:424], as are the restore page and the scheduled-backup token. A viewer cannot download the database.

The API and agent access

Five tools are exposed to an agent, of which one writes [src: app/controllers/mcp.php:62]: consignors, consignor detail, items and a settlement preview on the read side; recording a sale on the write side.

An agent cannot record a payout, and the instructions give the reason in one line: paying a consignor is cash leaving the till, and it stays with the shop [src: app/controllers/mcp.php:60].

The instructions also hand an agent the exact arithmetic — intdiv(sale*bp + 5000, 10000), round half up, integers only, the two shares always summing to the sale price with no penny lost or invented [src: app/controllers/mcp.php:57]. An automation reconciling the books can therefore reproduce the figures rather than approximating them.

Reports (3.2.0)

Reports shows a period — this month, last month, the last 90 days, this year or your own dates — as sales and the average sale, what the shop and the consignors earned, the sell-through rate, the average days from intake to sale and how far sale prices came in under list; then the same by category, the top consignors, and how old the stock on the floor is [src: app/controllers/reports.php:36]. Sell-through is items sold in the period divided by the items on the floor at its start plus those taken in during it; items since returned, donated or expired are left out, because when they left the floor is not recorded. The category table downloads as a CSV. Every figure is a sum or count of the ledger's own rows, added up in whole cents.

Average sale, staff and hours (3.3.0)

Average sale counts a basket rung up at the counter as one sale, however many items are in it, and a single-item sale as its own [src: app/controllers/reports.php:51]; the tile shows the average per sale and how many items each sale carries. By staff member shows each person's sales, items, takings and average sale. Every sale now records who rang it up — the signed-in person, or the owner of the API key an agent or script uses [src: app/src/Consign.php:204] — and the report names them from the team, or keeps the email of someone no longer on it [src: app/controllers/reports.php:53]. Sales from before 3.3.0 show as not recorded. By hour of day shows when sales were rung up, in your timezone, with the busiest hour as the longest bar. A sale entered on a later day than its sale date is left out of the hours, and the page says how many [src: app/controllers/reports.php:58].

Changing prices in bulk (3.2.0)

On Items, tick the rows and use the bar that appears: set a status (returned, expired, available), or change the price — set a new one, or take a percentage off for a markdown [src: app/controllers/v3.php:839]. Only items still on the floor change price: a sold item's price is on its sale, and a returned or expired one is not for sale. A markdown rounds to whole cents; the change is one transaction and is audited.

Drop-off booking (3.2.0)

Drop-offs sets the weekly times you take items in (a day, opening and closing time, slot length, how many people a slot takes) and switches on a public booking page at /dropoff, off on a fresh install [src: app/controllers/dropoff.php:22]. A consignor, or someone new, picks a free time and gives a name, an email, an optional phone number and a line about what they are bringing; the page shows a cancel link, and so does the confirmation email when your mail server is set up [src: app/controllers/dropoff.php:39]. A booking made with the email of a consignor you already have is linked to them.

A slot never takes more people than its capacity: each booking claims a place with one conditional update on that slot's counter, so two people pressing Book at the same moment cannot both take the last place [src: app/src/Dropoff.php:105]. A slot closes an hour before it starts [src: app/src/Dropoff.php:21]. The page is public, so it is bounded: a hidden field turns away form-filling robots [src: app/controllers/dropoff.php:47], one email address holds at most two upcoming bookings, and one connection makes at most five bookings a day [src: app/src/Dropoff.php:19].

The Drop-offs list shows who is coming each day and what they are bringing; mark each booking arrived, a no-show or canceled. Canceling gives the place back [src: app/src/Dropoff.php:117]. Once someone has arrived, Take in items opens the intake form for that consignor.

Item entry from a photo (3.2.0)

On the intake form, Fill from a photo sends a photo of the item to the AI provider you choose in Settings and puts a draft title, category and notes line (brand, size, color, condition, a short description) into the form for you to check [src: app/src/Llm.php:50]. It uses your own account: OpenAI or Anthropic with your own API key, or a model on your own machine through Ollama. It is off until you choose a provider.

The photo goes from your server to that provider in one request, and Consigna keeps no copy. Its type is read from the file itself, so a document renamed to .jpg is refused before anything is sent [src: app/controllers/itemai.php:17]. The draft uses one of your own categories when one fits. It never suggests a price [src: app/src/Llm.php:40] and never saves anything: the item is checked in only when you press the button. The API key is not written back into the Settings page, the audit trail or the shareable backup.

What Consigna does not do

It is not a fiscal till. The counter records sales and how they were paid; it has no fiscal signature, no certified security device and no connection to any tax authority. Where your country has rules for tills, check what applies to your shop with your tax adviser before you use the counter as your till.

It does not let an agent pay anybody [src: app/controllers/mcp.php:60].

It does not move money. A payout is a record that cash left the till; the cash leaves by whatever means you already use. The bank file is a set of instructions for your bank, which moves the money when you approve it [src: app/src/BankFile.php:5].

It does not price items for you. The photo draft leaves the price to you [src: app/src/Llm.php:40].

It does not lose or invent a penny [src: app/src/Consign.php:10], and it does not let a split change rewrite a past sale [src: app/src/Consign.php:14].

It does not let a sale be undone after the consignor has been paid for it [src: app/src/Consign.php:232].

It does not let the shop floor change a consignor's terms [src: app/controllers/v3.php:23], because the split is the agreement.

Looking after your install (from 4.0)

Updates from inside Consigna

Updates in the menu opens the Updates page (/updates) [src: app/views/updates.php]. Paste your license key once and press Save key: either key works, the one in your Lemon Squeezy receipt or the one on your order page (it starts with OWX-). Check for updates asks the store for the newest release your license covers; Consigna never checks by itself.

When a new version is ready, Back up and update to that version backs up your data and the current version, installs the new one and checks it. If the update is interrupted, or the new version's database step or that check fails, the previous version is put back by itself. Consigna is unavailable for about a minute while this runs. The update replaces Consigna's files, including any you edited, except .htaccess, .user.ini and robots.txt: an edited copy of those is kept, with the new version's copy beside it as .new to compare and merge. The Health page lists any file that differs from the version installed, so you can see that before you update. A copy of the previous version (data/updates/code-<version>/) and a backup of your data from just before the update stay in data/updates/ [src: app/src/Updater.php]. If Consigna shows an error page after an update anyway, that is your way back: copy the files from code-<version>/ over the app's files, then restore the backup on the backup page. Updating from the page needs PHP's sodium and Phar extensions; if one is missing, the Updates and Health pages say so and updates stay by hand. Where Consigna is hosted for you, the host updates it and the page says so.

Reaching 4.0 from a 3.x version is the last update done by hand: replace the files as before (config.php and data/ stay, and the database migrates itself on the next page load). From 4.0 on, the Updates page does it.

Starting with sample data

The installer offers Start with sample data: a resale shop's consignors, items, sales and payouts to try things on. You can remove it in one click. [src: app/install/index.php:126]. It is ticked by default; untick it to start empty. The sample is added when you open the first page after the installer.

When you are done with it, press the remove button in the sample banner. It asks "Remove the sample consignors, items, sales and payouts? Your own records stay." Only the sample goes: your own records stay, and a sample record that one of your own records uses is kept and named in the report [src: app/src/SampleData.php].

Backups by themselves, and a copy somewhere else

On the Backup & restore page (/restore), Back up once a day by itself is on by default; untick it to stop [src: app/views/restore.php:80]. The first visit at least 24 hours after the last backup starts the next one, after the page has loaded, so no cron job is needed. The newest 14 stay in data/backups/, with credentials left out of them (a restore keeps the live values for those). Back up now takes one straight away.

To keep a copy somewhere else as well, fill in an S3-compatible bucket (Backblaze B2, Wasabi, AWS S3 or your own MinIO): the bucket address (an https endpoint), the region, the bucket name, an optional folder, and the access key ID and secret key. Leave the address empty to keep backups on this server only. The copy goes only to the bucket you set; nothing is sent to Ownware. Once a week Consigna reads the newest backup back through the restore's own dry run and keeps the result for the Health page [src: app/src/BackupTargets.php].

On Nginx: keep data/ private

On Apache, the .htaccess that ships with Consigna keeps data/ (your backups included), src/, config.php and the other private files off the web. Nginx does not read .htaccess, so add these three blocks to your server block, above the block that passes .php files to PHP (Nginx uses the first one that matches):

location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
location ~ ^/views/.+\.php$ { deny all; }
location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }

Then open /config.php in a browser: it should answer 403. Only the template files under views/ are closed, not the folder: the app's own saved-view addresses start with /views/ too, and they must reach it.

A complete server block for a new site (PHP-FPM 8.3; change the name, the folder and the socket to yours):

server {
    listen 80;
    server_name example.com;
    root /var/www/consigna/app;
    index index.php;

    location ~ ^/(src|data|bin|tests|controllers|deploy|cron)/ { deny all; }
    location ~ ^/views/.+\.php$ { deny all; }
    location ~ (^|/)(config\.php|\.demo-mode)$|\.sqlite$ { deny all; }

    location = /install/ { rewrite ^ /install/index.php last; }
    location / { try_files $uri /index.php?$query_string; }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

The Health page

Health in the menu (/health) shows, one line each, whether the install is looking after itself: the PHP version and extensions, disk space, whether data/ is writable, email, the age of the last backup, the offsite copy, the weekly check, whether an update is waiting, and any file that differs from the version installed. Each line says what to do when it is not fine. The page only reads: it never changes anything and never contacts anything [src: app/src/Health.php].

Connect it to other tools (Zapier, Make, n8n)

Consigna sends a webhook the moment something happens and accepts calls to its API, so Zapier, Make and n8n can connect to it without a plugin. The steps, the events and example recipes are in API.md, in the section "Connect it to Zapier, Make or n8n"; the API page of Consigna on the store shows the same file [src: app/API.md].

New in 4.1.2: sign-in limits

New in 4.1.2 (security update): Two-step codes are now limited (10 wrong per person or per address in 10 minutes), and wrong passwords count per account as well as per address. An installed app whose database does not answer keeps its web installer closed instead of showing the install form. Single sign-on starts a fresh session. On Ownware Cloud, opening the app from the console starts a fresh session.

Wrong passwords are counted per address and, since 4.1.2, per account too (the email typed, in any case, stored only as a hash). After 10 wrong passwords in 10 minutes the sign-in at /login answers "Too many attempts — wait a few minutes and try again." for that address, and for that account from any address. A right password clears both counts [src: app/src/LoginRate.php:40].

Wrong two-step codes (for people who turned two-step sign-in on under /security) are counted per person and per address. After 10 wrong codes in 10 minutes the code step answers "Too many wrong codes. Wait ten minutes, then sign in again. A recovery code works in place of the code." even for the right code, and the half-finished sign-in is dropped, so the password is asked again [src: app/src/LoginRate.php:91]. The right code clears that person's count; an address's count runs out ten minutes after its wrong codes. Before 4.1.2 the code step had no limit.

If you are locked out: wait ten minutes, then sign in again with your password and a current code, or one of your recovery codes (each works once). If you lost both the authenticator and the recovery codes, whoever runs the server can turn two-step sign-in off for you in Consigna's database (by default the SQLite file in data/, or your MySQL database): UPDATE users SET totp_secret = '' WHERE email = 'you@example.com'; Then sign in with your password and turn it on again under /security.

Single sign-on now starts a fresh session too [src: app/controllers/v3.php:642].

On Ownware Cloud, opening Consigna from the console starts a fresh session [src: app/index.php:58].

The web installer stays closed when the database does not answer: with config.php in place, /install now answers 503 "This app is already installed (config.php exists), but its database is not answering, so the installer stays closed." and writes nothing until the database answers again. Before 4.1.2 a database outage (or a missing SQLite file) showed the install form there, and whoever found it could set up a new administrator. To install again from scratch, remove config.php first [src: app/install/index.php:20].

New in 4.1.3: security update

New in 4.1.3 (security update): no other site can put Consigna's pages in a frame; the /mcp address refuses requests from other sites' pages; company sign-in is stricter; and the audit CSV treats one more first character as text. Nothing else changed: no feature, screen, setting or database table, and your data, config.php and data/ are not touched.

No other site can frame these pages. Every page now answers Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so a page on another site cannot show Consigna in a hidden frame and trick a signed-in person into pressing a button [src: app/index.php:4]. The installer answers frame-ancestors 'none' and X-Frame-Options: DENY [src: app/install/index.php:4]. If you had Consigna framed inside another site of your own, that frame now stays empty; link to it instead.

/mcp refuses other sites. A request whose Origin header names another site gets 403 before anything is read. A desktop or server client sends no Origin and is served as before, and so is a page at Consigna's own address [src: app/src/Mcp.php:272].

Single sign-on. The sign-in now uses PKCE (S256), so an intercepted sign-in code is of no use on its own; a sign-in that comes back to a different browser session is asked to start again [src: app/src/Oidc.php:228]. With Google as the provider, a company address needs a Google Workspace account of that same domain (the token's hd claim), and Google must report the address as verified, so a personal Google account made with a work address no longer gets in [src: app/src/Oidc.php:215]. Gmail addresses work as before, other providers are unchanged, and password sign-in is not affected.

The audit CSV. A cell that starts with a carriage return now opens in a spreadsheet as text, as cells starting with = + - @ or TAB already did [src: app/src/AuditLog.php:138].

Docker. The Dockerfile now installs PHP's zip extension, so a Docker install can make the backup with files [src: app/Dockerfile:7].

Also changed, with no effect on a running install: the corner card on a public demo offers only the purchase, and API.md no longer shows the config example of a product we retired.

← Back to Consigna · Quickstart · API · Test run