User guide
MSP Store
Looking for what it does rather than how to use it? Read the MSP Store overview .
The MSP Store is a per-MSP storefront: your clients browse the products and services you sell, at the price you set for them, and place orders that land in your queue for approval. Approved orders are fulfilled by you and invoiced through Solidlio’s normal invoicing on net terms.
This guide covers the MSP-side store administration, the client-side buying experience, and the platform-side controls. Store payouts and the reserve engine are documented here too, with their current activation requirements stated plainly.
Concepts
| Concept | What it is |
|---|---|
| Store | Not a separate object. Your store is your account’s slice of the shared product catalogue, plus your own custom items. |
| Catalogue product | A distributor-sourced product (CatalogProduct). Shared across all MSPs; you control visibility and price, not the record. |
| Custom item | A product or service you author yourself (MspCustomItem, mci_). SKU, price, images, feature table, documents. |
| Product override | Your per-product decision: hide it, rename it, or pin a fixed price (MspProductOverride). |
| Vendor preference | Allow or block a distributor for your store, with an optional per-vendor markup (MspVendorPreference). |
| Pricing profile | Your markup policy: base multiplier, per-category and per-brand rules, minimum-margin floors, rounding, FX buffer. |
| Org pricing rule | A negotiated price for one client organization — contract price, discount, or volume break (PricingRule, prc_). |
| Bundle | A fixed price for a group of items. Expands server-side into its component lines at checkout. |
| Store order | A client’s order (MspStoreOrder, mso_), with snapshotted line items (MspOrderItem, moi_). |
| Store transaction | The fee ledger row for an order (MspStoreTransaction, mstx_): subtotal, platform fee, processing fee, net to you. |
| Transaction fee rule | The platform’s commission schedule (TransactionFeeRule), by category and item bucket, with minimum and maximum. |
| Reserve account | Risk withholding on card settlements (MspReserveAccount, mra_), with held funds, a ledger and a risk tier. |
| Payout | A Stripe Connect transfer of settled card revenue to you (MspPayout, pyo_), net of the reserve withholding. |
Roles and permissions
The store’s read surfaces are deliberately open to every authenticated role — a client has to be able to browse the store they buy from. Writes are floored at MSP administrator, with checkout as the one deliberate exception.
| Action | CUSTOMER | power user | organization administrator | MSP technician | MSP administrator | platform administrator |
|---|---|---|---|---|---|---|
| Browse products, bundles, categories, brands | ● | ● | ● | ● | ● | ● |
| See cost price, margin and vendor markup | — | — | — | ● | ● | ● |
| Place an order (checkout) | ● | ● | ● | ● | ● | ● |
| See own organization’s orders | ● | ● | ● | ● | ● | ● |
| See every order in the account | — | — | — | ● | ● | ● |
| Store dashboard stats, vendor list, profile | — | — | — | ● | ● | ● |
| Approve / reject / fulfil an order | — | — | — | — | ● | ● |
| Edit pricing profile, vendors, custom items | — | — | — | — | ● | ● |
Settlements (/msp/accounting/settlements) | — | — | — | — | ● | ● |
| Store access, fee rules, reserve admin | — | — | — | — | — | ● |
Two rules do the heavy lifting:
- A client sees the selling price and nothing else.
- Buyers are pinned to their own organization. Your account holds many client organizations, so account scoping alone is not isolation for a buyer.
Walkthrough 1 — Set up your store
Everything here is MSP portal, MSP administrator.
- Set your markup —
Store & Catalog → Pricing Profiles(/msp/store/pricing). Choose a base multiplier (1.2= cost + 20%), add per-category and per-brand markups, set minimum-margin floors, and pick a rounding rule (NONE,NEAREST_CENT,NEAREST_DOLLAR,UP_TO_99,UP_TO_95). Optionally set a currency buffer to hedge cross-currency cost. - Choose your distributors —
Store & Catalog → Vendor Preferences(/msp/store/vendors). Allow or block each vendor and set a per-vendor markup override. Blocking is plan-gated (see Plan tiers). - Curate the catalogue —
Store & Catalog → Product Catalog(/msp/store/catalog). Set each product toVisible,HiddenorApprovalfrom the dropdown on its row. Renaming a product and pinning a fixed price are supported by the API but cannot be done from this page — see Limits. - Add what the catalogue does not carry —
Store & Catalog → Custom Items(/msp/store/custom-items). Author your own products and services with a SKU (auto-generated or hand-typed), price, images and documents.ACTIVEand notHIDEis what makes one sellable. The specification/feature table on the edit page does not save — see Limits. - Negotiate per client — client-specific contract prices, percentage discounts and volume breaks are
PricingRulerecords, managed underSettings → Pricing. They apply on top of your markup for that client only.
Walkthrough 2 — A client places an order
Org portal (/org/*) and end-user portal (/user/*) render the same storefront.
- Browse —
Shop(/org/shop,/user/shop). Faceted search over the products you have made visible, priced for that client’s organization. Semantic search is available where the account has it enabled. - Bundles —
/org/bundles,/user/bundles, linked from the storefront. - Cart —
/org/cart,/user/cart. Every line price is re-priced by the store engine, so line prices and the subtotal match what checkout will charge. The cart’s tax and total are not — it applies a flat 5% placeholder. Read the subtotal, not the total; real tax is computed at order time. See Limits. - Checkout —
/org/checkout,/user/checkout. The buyer picks a payment path, adds a shipping address and notes, and places the order. - The order lands
PENDING_APPROVALin your queue. The buyer sees it underOrders(/org/orders,/user/orders) with a detail page per order.
Every unit price is recomputed server-side by the same engine the product page used.
Walkthrough 3 — Approve, fulfil and invoice
MSP portal, Store & Catalog → Client Orders (/msp/store/orders).
- Review the order. AI triage is available per order and stores its result on the record.
- Approve or Reject. Approving stamps
approvedAt/approvedBy. Rejecting setsCANCELLEDand records the reason in the notes. Both are only valid fromPENDING_APPROVAL. - Fulfil — advance the order forward through
PROCESSING → SHIPPED → DELIVERED, adding a carrier and tracking number. - Invoicing is automatic. The
msp-order-invoicingjob selectsDELIVERED, un-invoiced orders onNET30/NET60and raises the invoice, writing the invoice id back onto the order.
Order states
| State | Reached by | Next |
|---|---|---|
PENDING_APPROVAL | Checkout | APPROVED or CANCELLED |
APPROVED | Approve | PROCESSING, SHIPPED or DELIVERED |
PROCESSING | Fulfil | SHIPPED or DELIVERED |
SHIPPED | Fulfil | DELIVERED |
DELIVERED | Fulfil | Terminal. Invoiceable on net terms. |
CANCELLED | Reject | Terminal. |
How a price is computed
The steps run in this order:
- Pinned price wins. A per-product custom price replaces the whole calculation. It is converted into the buyer’s currency but is never buffered, marked up, floored or rounded — it is your hand-typed number.
- Cost, converted from the product’s cost currency into the buyer’s sell currency. A missing or stale FX rate fails closed — the line becomes unavailable rather than quoting a wrong-currency price.
- FX buffer, applied to cost, and only on a cross-currency sale.
- Base multiplier from your pricing profile.
- Category markup, then brand markup (stored as fractions:
0.20= +20%). - Vendor markup override (stored as a percentage:
10= +10%). - Minimum-margin floors — category then brand. A price below
cost ÷ (1 − margin)is raised to it. - Rounding rule.
It applies the single best applicable rule rather than stacking them, so two discounts can never compound into a margin-destroying price. It can only ever lower a price, and it does not enforce the margin floors — a below-cost contract price for a client is a call you made on purpose.
Tax
Tax comes from the shared tax implementation, never a local table. A Canadian order with a missing province tells you to set the province; anything outside a configured jurisdiction tells you to review before invoicing.
Fees
The platform commission comes from TransactionFeeRule — percentage, minimum, maximum, category and item bucket. Items are grouped by the rule that governs them and each group is charged clamp(pct × groupSubtotal, minimum, maximum).
Configuration
Pricing profile (per MSP)
| Setting | Effect | Unset |
|---|---|---|
baseMultiplier | Multiplies cost. 1.2 = cost + 20%. | No base markup applied. |
categoryRules | Per-category markup and minimumMargin, both fractions. | No category adjustment. |
brandRules | Per-brand markup and minimumMargin, both fractions. | No brand adjustment. |
roundingRule | NONE, NEAREST_CENT, NEAREST_DOLLAR, UP_TO_99, UP_TO_95. | Rounds to nearest cent. |
currencyBuffer | Extra hedge on converted cost, cross-currency sales only. Negative is refused. | No buffer. |
Plan tiers
The store carries exactly one plan gate: catalogControl, which governs vendor blocking. Everything else — the storefront, pricing profiles, custom items, orders, fulfilment, invoicing — is available on every tier.
catalogControl | Effect |
|---|---|
false | No vendor control. Blocks are stored but not applied. |
"LIMITED" | You may block third-party vendors, but not the platform catalogue. |
"FULL" | You may block any vendor. |
A refused block returns a 402-style upgrade envelope naming the tier required, so the UI can offer the upgrade in place.
Platform administration
| Page | What it controls |
|---|---|
/platform/store-access | Per-account and per-organization store access toggles. See Limits below. |
/platform/store-settings/fees | TransactionFeeRule schedule — the commission actually charged at checkout. |
/platform/store-settings/pricing | Oversight of every MSP’s pricing profile. |
/platform/finance/reserves | Reserve accounts, held funds, chargebacks, ledger, risk tiers, manual early release. |
All platform store routes require platform administrator or SUPER_ADMIN.
Known defects on these pages — read before relying on them:
- The rules themselves are live at checkout — only the admin view of them is blank. “Analyze Fees” consequently sends an empty rule set and spends AI credits analysing nothing.
- Category markups set in the platform pricing-profile modal change no price. That modal keys rules by fee-category name (
HARDWARE,SOFTWARE,SERVICES,SUBSCRIPTIONS), while the engine looks them up by catalogue category id. Use the MSP-facing page (/msp/store/pricing), which keys them correctly. profileType(AGGRESSIVE/PREMIUM/ …) affects nothing. The pricing engine never reads it. “Apply Suggestion” on an AI pricing recommendation writes only this field.- Store-access AI analysis never reaches the model. The proxy omits the caller role and sends an empty account id, so the request is refused for missing AI account context and the canned heuristic fallback is always what you see.
- Store-access changes are not audit-logged and the toggles bypass the confirmation modal entirely, so disabling access takes one click with no record.
- Fee rules are not validated for
maximumFee >= minimumFee. A rule with a maximum below its minimum silently clamps every governed order to the maximum.
Payouts and the reserve
This applies to card-settled orders only. Net-terms orders are money you collect yourself through your own invoice; nothing in the payout engine touches them.
- The daily payout run (06:00 UTC) sweeps
SETTLED, unpaid transactions for each connected account, FX-converts each into your region currency, and refuses the whole account if a rate is missing or older than 48 hours. - Matured holds are released nightly back into your available balance, which is paid out on the next run.
- A dispute deducts held funds first, then the available balance — which is allowed to go negative. Every movement writes a
ReserveLedgerEntry.
Seeded risk tiers
| Tier | Withheld | Held for | Qualifies at |
|---|---|---|---|
NEW | 10% | 30 days | Default for a new account. Chargeback rate under 2%. |
ESTABLISHED | 5% | 14 days | 90+ days old, chargeback rate under 1%. |
TRUSTED | 2% | 7 days | 365+ days old, under 0.5%, $10,000+ of settled sales. |
HIGH_RISK | 15% | 45 days | Fallback tier. No thresholds. |
Tiers are re-evaluated nightly against live metrics and the change is emailed to you.
Troubleshooting
| Message | Cause |
|---|---|
Product not available: <sku> | Hidden by your product override, inactive, or from a vendor you block. |
Product has no sellable price: <sku>. Set a cost price, a pricing profile or a custom price. | The product has no cost and no pinned price, so the engine has nothing to mark up. |
No exchange rate to price <sku> in <currency>. | FX rate missing or stale. Checkout refuses stale rates outright — check fx-rate-sync. |
Item not available: <id> / Item has no sellable price: <sku> | Custom item is not ACTIVE, is set to HIDE, or has a zero price. |
Order cannot be approved in current status | Only PENDING_APPROVAL orders can be approved or rejected. |
Cannot move a <status> order to <status>. | Fulfilment is forward-only. Approve first; DELIVERED and CANCELLED are terminal. |
MSP staff access required | The surface exposes account-wide commercials. MSP technician or above. |
Organization context required | A non-staff caller with no organization header. Spend and orders fail closed. |
The selling MSP has not connected a Stripe account to receive card payments | No Stripe Connect account. See Limits — there is no self-serve onboarding today. |
Your <tier> plan does not include vendor control. | catalogControl is false on this tier. |
The <vendor> cannot be blocked on your <tier> plan. | catalogControl is LIMITED; the platform catalogue cannot be blocked. |
[TAX HOLD] in the order notes | Tax jurisdiction unresolved. Tax was not guessed. Review before invoicing. |
SKU already exists | Custom item SKUs are unique within your account. |
Sell price cannot be below cost price | Guard on custom item and service pricing. |
Limits and known behaviour
These are current, verified limitations. They are stated here rather than omitted, because each one changes how you should configure or sell the store.
Payment methods
- Net 30 and Net 60 work end to end. The order is invoiced automatically once delivered.
- The cart checkout and the Bundles & Kits terms dropdown both gate on it, and an older response with the field absent reads as unavailable — the failure direction is “offer purchase-on-account”, never “offer a card that cannot be charged”.
- What has changed is that buyers are no longer shown a card option in the meantime.
PREPAIDis refused at checkout. Nothing settles it — it is not charged (requiresPaymentis card-only) and not invoiced (the invoicing job selects NET30/NET60) — so a prepaid order produced goods with no billing event.POST /store/ordersnow answers400 … cannot be settledfor it, and it has been removed from the Bundles & Kits dropdown. Re-enable it once a prepaid balance model exists to settle against (isSettleableTermsis the single place to change).- Net-terms transactions settle when their invoice is paid. Card orders settle on the
payment_intent.succeededwebhook; a net-terms order has no intent, so its transaction movesPENDING→SETTLEDwhen the invoice raised for it is markedPAIDby the reconciler, inside that same transaction. > Previously only card orders ever reachedSETTLED, so platform commission > accrued on net-terms business was recorded and never collected, and those > transactions never appeared in a settlement.
Buyer-facing
-
The cart shows no tax line. It previously computed
subtotal × 0.05and labelled it GST in whatever currency the cart was in — a number the platform would never charge. Real tax is jurisdiction-resolved server-side at order time, and the cart now shows the subtotal only rather than a total it cannot stand behind. -
Buy bundles from the bundles page instead, which places the order directly.
-
Unavailable and unpriceable lines render as normal. The cart API returns a per-line
availableflag and the product API returnspriceable, and nothing reads either. A product you have since hidden still shows at its stale price but is excluded from the subtotal, so the cart does not add up; a product with no sellable price shows$0.00with Add to Cart enabled. Checkout then refuses the whole order. -
Order notes and PO number are write-only. The checkout PO field only renders on the net-terms path, and
order.notesis displayed in neither buyer order view. Cart notes are dropped unless the buyer chooses “Request Quote”. -
The customer portal has no cart affordance.
/user/cartand/user/checkoutare in no navigation and the portal chrome has no cart icon; the only way back to the cart is the mini-cart on the storefront page itself. -
The fee ledger is staff-only. A buyer’s checkout response carries the transaction’s
id,statusandsubtotalCents— what a receipt needs — and no longerplatformFeeCents,platformFeePercent,processingFee*ornetAmountCents. Those are the commercial terms between Solidlio and the selling MSP, and the checkout response was the one place the buyer-stripping rule was not applied.
MSP administration
- Only the visibility dropdown is wired.
- Only local state changes; the table is empty again on reload. Images, documents and enrichment on the same screen do save.
MspCustomItem.salesCountis never incremented, so the custom-items “Total Revenue” and “Gross Profit” tiles and every card’s “0 sold” are zero by construction.- The AI triage panel is read-only. Its editor — risk and priority selects, recommendation, add/remove action, Save Changes, Apply Auto-Approve — is gated on an
editableprop the orders page never passes. - Brand markup rules on
/msp/store/pricingpersist but do nothing for products whose brand the engine cannot match; the page documents this itself.
Platform controls
- The Store Access toggles do not gate the store.
Account.storeEnabledandOrganization.storeAccessEnabledare read only by the organization-services listing, to compute a display-onlyeffectiveAccessflag. Turning store access off does not stop browsing, carting or ordering. - Category and brand overrides are write-only.
MspCategoryOverrideandMspBrandOverridestore visibility, markup type and markup value, and the categories list echoes them back — but the pricing engine reads the pricing profile’scategoryRules/brandRulesinstead, and the product query filters on product-level overrides only. Setting a category toHIDEhides nothing. payoutScheduleis not read. It is stored and displayed, but the engine runs daily for every connected account regardless of the setting.- Reserve tier terms are not editable from the UI. `PUT
- Pricing profiles cannot be created from the platform admin. `POST
brandRulescannot be set from the platform admin router — its create and update schemas omit the field. Only the MSP-facing pricing page can set it.- Store-access reporting counts the wrong rows. The store-access statistics, per-account order counts and activity feed read
ShipmentandAccountingDocumentrather thanMspStoreOrder/MspStoreTransaction, so an unshipped store order counts as zero and unrelated invoices count as store revenue.
Not reachable from any UI
The per-order fee ledger is on the order detail for staff. GET /store/orders/:orderId now includes the order’s MspStoreTransaction — platform fee, processing fee and net amount — for MSP technician and above, so an order’s fees can be reviewed after the fact. GET /store/transactions still has no UI for the account-wide list.
The ledger used to be returned exactly once, in the checkout response, to the buyer — while the seller had no way to see it at all.
On the platform side the same is true of GET /store-access/accounts/:id/organizations, GET /store-access/accounts/:id/activity and POST /store-access/bulk/accounts — each has a hook with no caller, so bulk enable/disable and the per-account activity feed cannot be used.
Service Marketplace
The MSP portal’s Service Marketplace (/msp/marketplace) is a separate, transactable feature with its own documentation: listings, subscriptions, recorded sales with a three-way split, fulfilment, reviews and platform moderation. A store service that is backed by a marketplace subscription cannot be relisted, and its price is owned by the subscription rather than the store.