User guide
Software Asset Management
Looking for what it does rather than how to use it? Read the Software licences overview .
What it is
Software Asset Management is the record of every software title your organization is entitled to run, who or what is using each seat, and what is actually installed on your hardware. Comparing those three things is what tells you whether you are over-paying, under-licensed, or about to lose access at a renewal you forgot.
Concepts
| Concept | What it is |
|---|---|
| Software vendor | The publisher (Microsoft, Adobe). Carries support contacts, a documentation URL and an admin-console URL template. |
| Software product | A catalogue entry for one title, optionally with version and edition. Private to your account, or in the shared library. |
| Software library | The platform-wide catalogue of approved products and vendors, readable by every tenant. |
| Software licence | A purchased entitlement held by one organization: model, seat count, dates, cost, keys. |
| Seat assignment | One seat of a licence consumed by a person or by an asset. |
| Pool checkout | For concurrent (floating) licences: one live slot, held by an agent and released on check-in or expiry. |
| Installation | An observed copy of a product on an asset. Recorded manually or discovered by an RMM. |
| Compliance position | Seats held vs seats assigned vs copies installed, evaluated per product per organization. |
Licensing models
PER_SEAT, PER_DEVICE, SITE, VOLUME, CONCURRENT, SUBSCRIPTION, PERPETUAL, OPEN_SOURCE, FREEMIUM, METERED, OTHER.
Only PER_SEAT and PER_DEVICE are evaluated for seat compliance. CONCURRENT is evaluated against its pool. Every other model is reported as compliant — there is no seat count to compare against.
Licence statuses
ACTIVE, EXPIRED, SUSPENDED, CANCELLED, PENDING_ACTIVATION, PENDING_RENEWAL, TRIAL, GRACE_PERIOD.
Compliance reads ACTIVE, PENDING_RENEWAL, GRACE_PERIOD and TRIAL. The over- and under-licensed reports narrow further to ACTIVE and PENDING_RENEWAL.
Roles and permissions
Every software mutation requires organization administrator or above. Reads are limited by tenant scope only, so a CUSTOMER can see their own organization’s licences but change nothing.
| Action | CUSTOMER | power user | organization administrator | MSP technician | MSP administrator | platform administrator |
|---|---|---|---|---|---|---|
| View licences, products, installations, compliance | ● | ● | ● | ● | ● | ● |
| Create / edit / delete a licence | — | — | ● | ● | ● | ● |
| Assign and revoke seats | — | — | ● | ● | ● | ● |
| Create / edit / delete a product or vendor (own account) | — | — | ● | ● | ● | ● |
| Record, edit or remove an installation | — | — | ● | ● | ● | ● |
| Run product / vendor enrichment | — | — | ● | ● | ● | ● |
| Submit a product to the shared library (promote) | — | — | ● | ● | ● | ● |
| Concurrent-pool checkout / check-in / cleanup | — | — | ● | ● | ● | ● |
| Approve or reject a product into the shared library | — | — | — | — | — | ● |
Publish a vendor into the shared library (isPublic) | — | — | — | — | — | ● |
| Mark a vendor verified | — | — | — | — | — | ● |
| Read the platform library stats and moderation queue | — | — | — | — | — | ● |
MSP technician ranks above organization administrator in Solidlio’s hierarchy, so a floor set one notch too high locks the whole organization portal out. The floor here is organization administrator everywhere, and the web UI gates on exactly the same line (useCanManageSoftware), not on isStaff — which is MSP-only and would refuse the org admins the API accepts.
Tenant scoping is separate from role. A platform admin is unscoped; a customer sees only their own organization; MSP and org staff see every organization inside their account. A request with no tenant context matches no rows.
Library visibility
A product or vendor you create belongs to your account and is invisible to every other tenant. It enters the shared library only through moderation:
PRIVATE --promote--> PENDING --approve--> PUBLIC
\--reject---> REJECTED
Vendors have no promote step; a platform administrator flips isPublic directly. Neither isPublic nor isVerified can be set by a tenant — they are platform trust signals shown to everyone.
Walkthroughs
Record a licence you have bought
- Go to Software → Products and confirm the title exists. If it does not, choose Add Software, pick the vendor, category and type. New products start
PRIVATEto your account. - Go to Software and choose + Add License, or Software → Licenses → New.
- Pick the product and the owning organization, then the licensing model.
- Record the seat count (
totalSeats), the term dates and the cost. LeavetotalSeatsempty for an unlimited licence. - Record the identifiers you will need later: licence key, serial number, agreement number, subscription ID, vendor account ID. They appear on the licence’s Identifiers & Access panel afterwards, with the key masked until you choose Reveal.
- Save. The licence detail page opens.
Assign and reclaim seats
- Open the licence and choose Manage Seats.
- Choose User, Contact or Asset, pick the holder, optionally set an expiry, and choose Assign. Anyone already holding a seat is filtered out of the picker.
- The seat counter increments immediately. When the licence is full the API refuses further assignments with “No available seats for this license” — the seat claim is atomic, so two simultaneous assignments cannot oversubscribe a licence.
- To reclaim a seat, choose Revoke on its row and confirm. The assignment moves to
REVOKED, the reason is stored, and the seat is immediately available again.
Work the compliance dashboard
- Go to Software → Compliance. The score is the share of evaluated licences that are compliant.
- Over-Licensed lists licences with more than 30% of their seats unused, with the annual value of the waste. A licence with no recorded cost shows “cost not recorded” — never
$0. - Under-Licensed lists licences where assigned seats or installed copies exceed the seats held, with the shortfall, a severity, and the estimated cost to true up. Choose Add License to buy the gap.
- Expiring lists everything due within 90 days, bucketed
CRITICAL(≤7 days),HIGH(≤14),MEDIUM(≤30),LOWbeyond that. - Unlicensed lists discovered installations with no licence behind them. Where you already hold a licence for that product in that organization, an Attach a licence… picker appears on the row; choosing one links the install and it leaves the report. Where you hold none, the row offers Add License instead.
- Overview is the per-product, per-organization position: seats held, copies installed, compliance level and variance.
Track renewals
- Go to Renewals in the sidebar. Choose a 30, 60 or 90-day window.
- Renewals are grouped Next 7 days, Next 30 days and Later, showing seats, expiry date, whether auto-renew is on, and the recurring cost.
- Needs action counts the renewals with auto-renew off.
- Independently of this page, the daily
asset-expiry-notificationsjob creates an in-appLICENSE_EXPIRINGnotification for every admin, manager and technician in the licence’s organization when the days-to-expiry lands exactly on one of the licence’srenewalReminderDaysvalues (default[30, 14, 7]). The notification links to that licence in the recipient’s own portal, andlastReminderSentsuppresses a repeat within 24 hours.
Manage a concurrent (floating) licence
- Create the licence with model
CONCURRENTand amaxConcurrentlimit. - The licence detail page shows a Concurrent Use panel: slots in use, slots available, and how many checkouts expired without being returned.
- Agents call the pool API to check a slot out and heartbeat it. Capacity is enforced by locking the licence row and counting live checkouts, so the pool cannot be oversubscribed.
- Choose Check in on a row to release a slot by hand, or Release N expired to reclaim every checkout whose expiry has passed.
Submit a product to the shared library (platform administrators)
- A tenant opens their product and chooses Promote to Library (
PRIVATE → PENDING). - A platform administrator opens Platform → Software Library, works the Pending Approvals queue, and chooses Approve or Reject. A rejection requires a reason.
- Approval records who approved it and when, and clears any prior rejection. Rejection does the reverse, so a row never carries both decisions at once.
- Platform → Software Library → Vendors lists every public vendor. Choose the Verify badge on a row to toggle the platform verification mark.
Configuration
| Setting | What it does | If unset |
|---|---|---|
totalSeats | The entitlement compliance measures against. | Licence is unlimited; compliance reports it as UNKNOWN. |
usedSeats | Maintained by the service on assign/revoke. Not editable directly. | — |
maxConcurrent | Slot ceiling for a CONCURRENT licence. | Pool is unbounded; checkouts never refuse. |
expirationDate | Drives the expiring report, the renewals dashboard and the reminder job. | Licence never appears in any expiry view. |
renewalDate | Shown on the licence and the expiring report. Informational. | Shown as -. |
renewalReminderDays | Days-before-expiry on which the reminder job fires. | Defaults to [30, 14, 7] at creation. |
lastReminderSent | Set by the job. Suppresses a repeat within 24 hours. | First matching threshold notifies. |
autoRenew | Reported on renewals and in the reminder message. | Treated as manual renewal. |
recurringCost + billingCycle | Annualised to price wasted spend and total annual spend. | Waste and exposure report null — “cannot price”, not $0. |
purchaseCost | Second-choice per-seat basis for risk exposure. | Falls through to the product MSRP. |
Product msrpPrice | Last-resort per-seat basis for risk exposure. | The licence counts as unpriced. |
licenseKey and friends | Displayed on the licence’s Identifiers & Access panel; key masked until revealed. | Panel omits the field. |
consoleUrl | Licence-level override for the vendor admin console link. | Falls back to the product’s, then the vendor’s URL template. |
contractId | Links the licence to a billing contract. | Licence stands alone. |
How compliance is computed
Read this before you argue with a number.
- Installations are counted per product per organization — never across an MSP’s whole account. One client’s copies can never make another client look over-deployed.
- Installed copies are compared against every seat that organization holds for that product, summed across all of its licences for the title. Two licences for one product add up; they are not each compared to the whole install count.
- Over-deployment is reported once per (product, organization) — on the licence with the most seats — so both the row count and the dollar exposure are reported once, not once per licence.
- Over-licensed means more than 30% of seats are unused.
- Under-licensed means assigned seats exceed seats held, or installed copies exceed the organization’s total seats for that product.
- Seats are priced from the licence’s own recurring cost annualised by its own billing cycle; failing that, its purchase cost; failing that, the product’s MSRP. Nothing is ever guessed. A figure that cannot be priced is reported as
null, withcostCoveragetelling you how many licences were priced and how many were not. - Wasted spend uses only the recurring cost — money you actually pay. A vendor MSRP is not your spend, so it is not used to price waste (it is used to price risk, where it is a fair proxy for a true-up).
Plan tiers
Software Asset Management carries no plan gate. Every tier gets licences, products, vendors, assignments, pools, installations, compliance reporting and renewals in full. There is no cap on licences, products or seats.
It does not apply to software records.
Troubleshooting
These are the actual messages the service returns.
| Message | Cause |
|---|---|
| No available seats for this license | Every seat is assigned. Revoke one or raise totalSeats. |
| Not enough available seats for this license | A bulk assignment larger than the free seats. It is refused in full. |
| This license is already assigned to this person/asset | Duplicate seat assignment. |
| Assignment is not active | Revoke or update was called on an already-revoked seat. |
| Cannot update non-active assignment | Expiry can only be changed while the assignment is ACTIVE. |
| Cannot delete license with active assignments | Revoke the seats first. |
| Cannot delete product with associated licenses | Delete or re-point the licences first. |
| Cannot delete product with associated installations | Remove the installation records first. |
| Cannot delete vendor with associated products | Same, one level up. |
| Only a platform administrator can change a vendor’s library visibility or verification | isPublic / isVerified were sent by a non-platform caller. |
| Only PRIVATE products can be promoted | The product is already pending, public or rejected. |
| Only PENDING products can be approved / rejected | Moderation acts on the pending queue only. |
| Product is already approved / already rejected | The library route was called on a settled product. |
| A vendor with this name already exists | Vendor names are unique per account. |
| Vendor has no website URL for enrichment | Set the vendor website before enriching. |
| This license does not support concurrent checkouts | Pool checkout used on a non-CONCURRENT licence. |
| License is not active | Pool checkout on a licence that is not ACTIVE. |
| No available concurrent slots | maxConcurrent reached. Check something in or run cleanup. |
| Checkout has expired (410) | Heartbeat arrived after the checkout expired. |
| Must provide checkoutId or personId | Pool check-in needs one of the two. |
| Installation already exists for this product/asset combination | One installation row per (product, asset). |
| License not found (400, on an installation update) | The licence is outside your tenant, or belongs to a different organization than the asset. |
| Person not found / Asset not found (400) | The assignment target is outside your tenant. |
| Organization not found (400, on licence create) | Cross-account licence creation. The destination must be inside your account. |
| Account context required to create a vendor / a product (403) | The request carried no account context. |
Limits and known behaviour
Stated plainly so you do not plan around them.
- The schema comment that claimed pgcrypto did this was wrong — pgcrypto is not installed anywhere; the encryption is done by the application.
- This is encryption at rest, not an access control. Every read path decrypts before responding, so anyone who can already read the licence through the API — including a
CUSTOMERin that organization — still sees the key. The Reveal toggle in the UI remains a display convenience. - Rows created before the change are still plaintext. The ciphertext envelope is self-identifying, so a plaintext key is detected and returned unchanged rather than throwing.
--dry-runreports without writing. licenseKeycan never be searched, filtered or sorted on. The ciphertext is randomised per write, so two rows with the same key look different. The licence search box matchesserialNumber,agreementNumberand product name — those three remain plaintext for exactly that reason.- Running the backfill under a different key than the services makes every key unreadable.
- This is encryption at rest, not an access control. Every read path decrypts before responding, so anyone who can already read the licence through the API — including a
- Concurrent pool checkout, check-in and heartbeat are agent APIs. The licence page shows live pool state and offers manual check-in and expired-slot cleanup, but there is no screen that checks a slot out — that is the job of the software holding the licence.
- It duplicates what
SoftwareLicensealready records (cost, billing cycle, commitment, renewal date, quantity) and records nothingSoftwareLicenselacks. The router, the api-client module and the hooks are now marked@deprecated.- The table and its routes are retained, not dropped. Rows may already exist and must stay readable and editable through the API.
- No UI will be built for it. Model new software spend as a
SoftwareLicense.
usedQuantity,status,purchaseDateandconsoleUrlonOrganizationSoftwareAssetare never written by its own routes.- Compliance evaluates only
PER_SEATandPER_DEVICE.SITE,VOLUME,SUBSCRIPTION,PERPETUAL,METEREDand the rest are reported compliant because there is no seat count to test.CONCURRENTis likewise reported compliant in the summary — its real position is the pool panel, which is not folded into the compliance score. - A licence with
totalSeatsunset isUNKNOWN, not compliant. It counts towardsunknownand lowers the compliance score, because the score’s denominator is every evaluated licence. - Seat assignment does not check the assignee’s organization. It checks the person or asset is inside your account. An MSP can therefore assign a seat on Client A’s licence to a person in Client B, both being inside the MSP’s account.
- There is no licence renewal action. Renewals are a reporting view and a notification; extending a term means editing
expirationDateby hand.autoRenewis recorded and reported but nothing acts on it. - Software changes are not written to the audit log. Licence, assignment, product and vendor changes are recorded only as the resulting row state. Library approval and rejection are the exception — they store who and when.
- Installations are the only compliance input. If nothing populates installations (no RMM integration, no manual entry), under-licensing can only be detected from assigned seats.
- The mirror stays — roughly twenty catalog/commerce tests depend on it.
- RMM-discovered vendors and products are private to the discovering account. They do not enter the shared library, so two tenants running the same title each get their own catalogue row.