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

ConceptWhat it is
StoreNot a separate object. Your store is your account’s slice of the shared product catalogue, plus your own custom items.
Catalogue productA distributor-sourced product (CatalogProduct). Shared across all MSPs; you control visibility and price, not the record.
Custom itemA product or service you author yourself (MspCustomItem, mci_). SKU, price, images, feature table, documents.
Product overrideYour per-product decision: hide it, rename it, or pin a fixed price (MspProductOverride).
Vendor preferenceAllow or block a distributor for your store, with an optional per-vendor markup (MspVendorPreference).
Pricing profileYour markup policy: base multiplier, per-category and per-brand rules, minimum-margin floors, rounding, FX buffer.
Org pricing ruleA negotiated price for one client organization — contract price, discount, or volume break (PricingRule, prc_).
BundleA fixed price for a group of items. Expands server-side into its component lines at checkout.
Store orderA client’s order (MspStoreOrder, mso_), with snapshotted line items (MspOrderItem, moi_).
Store transactionThe fee ledger row for an order (MspStoreTransaction, mstx_): subtotal, platform fee, processing fee, net to you.
Transaction fee ruleThe platform’s commission schedule (TransactionFeeRule), by category and item bucket, with minimum and maximum.
Reserve accountRisk withholding on card settlements (MspReserveAccount, mra_), with held funds, a ledger and a risk tier.
PayoutA 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.

ActionCUSTOMERpower userorganization administratorMSP technicianMSP administratorplatform 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.

  1. Set your markupStore & 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.
  2. Choose your distributorsStore & 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).
  3. Curate the catalogueStore & Catalog → Product Catalog (/msp/store/catalog). Set each product to Visible, Hidden or Approval from 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.
  4. Add what the catalogue does not carryStore & 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. ACTIVE and not HIDE is what makes one sellable. The specification/feature table on the edit page does not save — see Limits.
  5. Negotiate per client — client-specific contract prices, percentage discounts and volume breaks are PricingRule records, managed under Settings → 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.

  1. BrowseShop (/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.
  2. Bundles/org/bundles, /user/bundles, linked from the storefront.
  3. 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.
  4. Checkout/org/checkout, /user/checkout. The buyer picks a payment path, adds a shipping address and notes, and places the order.
  5. The order lands PENDING_APPROVAL in your queue. The buyer sees it under Orders (/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).

  1. Review the order. AI triage is available per order and stores its result on the record.
  2. Approve or Reject. Approving stamps approvedAt / approvedBy. Rejecting sets CANCELLED and records the reason in the notes. Both are only valid from PENDING_APPROVAL.
  3. Fulfil — advance the order forward through PROCESSING → SHIPPED → DELIVERED, adding a carrier and tracking number.
  4. Invoicing is automatic. The msp-order-invoicing job selects DELIVERED, un-invoiced orders on NET30/NET60 and raises the invoice, writing the invoice id back onto the order.

Order states

StateReached byNext
PENDING_APPROVALCheckoutAPPROVED or CANCELLED
APPROVEDApprovePROCESSING, SHIPPED or DELIVERED
PROCESSINGFulfilSHIPPED or DELIVERED
SHIPPEDFulfilDELIVERED
DELIVEREDFulfilTerminal. Invoiceable on net terms.
CANCELLEDRejectTerminal.

How a price is computed

The steps run in this order:

  1. 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.
  2. 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.
  3. FX buffer, applied to cost, and only on a cross-currency sale.
  4. Base multiplier from your pricing profile.
  5. Category markup, then brand markup (stored as fractions: 0.20 = +20%).
  6. Vendor markup override (stored as a percentage: 10 = +10%).
  7. Minimum-margin floors — category then brand. A price below cost ÷ (1 − margin) is raised to it.
  8. 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)

SettingEffectUnset
baseMultiplierMultiplies cost. 1.2 = cost + 20%.No base markup applied.
categoryRulesPer-category markup and minimumMargin, both fractions.No category adjustment.
brandRulesPer-brand markup and minimumMargin, both fractions.No brand adjustment.
roundingRuleNONE, NEAREST_CENT, NEAREST_DOLLAR, UP_TO_99, UP_TO_95.Rounds to nearest cent.
currencyBufferExtra 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.

catalogControlEffect
falseNo 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

PageWhat it controls
/platform/store-accessPer-account and per-organization store access toggles. See Limits below.
/platform/store-settings/feesTransactionFeeRule schedule — the commission actually charged at checkout.
/platform/store-settings/pricingOversight of every MSP’s pricing profile.
/platform/finance/reservesReserve 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.

  1. 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.
  2. Matured holds are released nightly back into your available balance, which is paid out on the next run.
  3. A dispute deducts held funds first, then the available balance — which is allowed to go negative. Every movement writes a ReserveLedgerEntry.

Seeded risk tiers

TierWithheldHeld forQualifies at
NEW10%30 daysDefault for a new account. Chargeback rate under 2%.
ESTABLISHED5%14 days90+ days old, chargeback rate under 1%.
TRUSTED2%7 days365+ days old, under 0.5%, $10,000+ of settled sales.
HIGH_RISK15%45 daysFallback tier. No thresholds.

Tiers are re-evaluated nightly against live metrics and the change is emailed to you.


Troubleshooting

MessageCause
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 statusOnly 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 requiredThe surface exposes account-wide commercials. MSP technician or above.
Organization context requiredA non-staff caller with no organization header. Spend and orders fail closed.
The selling MSP has not connected a Stripe account to receive card paymentsNo 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 notesTax jurisdiction unresolved. Tax was not guessed. Review before invoicing.
SKU already existsCustom item SKUs are unique within your account.
Sell price cannot be below cost priceGuard 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.
  • PREPAID is refused at checkout. Nothing settles it — it is not charged (requiresPayment is card-only) and not invoiced (the invoicing job selects NET30/NET60) — so a prepaid order produced goods with no billing event. POST /store/orders now answers 400 … cannot be settled for it, and it has been removed from the Bundles & Kits dropdown. Re-enable it once a prepaid balance model exists to settle against (isSettleableTerms is the single place to change).
  • Net-terms transactions settle when their invoice is paid. Card orders settle on the payment_intent.succeeded webhook; a net-terms order has no intent, so its transaction moves PENDINGSETTLED when the invoice raised for it is marked PAID by the reconciler, inside that same transaction. > Previously only card orders ever reached SETTLED, 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.05 and 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 available flag and the product API returns priceable, 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.00 with 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.notes is displayed in neither buyer order view. Cart notes are dropped unless the buyer chooses “Request Quote”.

  • The customer portal has no cart affordance. /user/cart and /user/checkout are 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, status and subtotalCents — what a receipt needs — and no longer platformFeeCents, platformFeePercent, processingFee* or netAmountCents. 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.salesCount is 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 editable prop the orders page never passes.
  • Brand markup rules on /msp/store/pricing persist 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.storeEnabled and Organization.storeAccessEnabled are read only by the organization-services listing, to compute a display-only effectiveAccess flag. Turning store access off does not stop browsing, carting or ordering.
  • Category and brand overrides are write-only. MspCategoryOverride and MspBrandOverride store visibility, markup type and markup value, and the categories list echoes them back — but the pricing engine reads the pricing profile’s categoryRules/brandRules instead, and the product query filters on product-level overrides only. Setting a category to HIDE hides nothing.
  • payoutSchedule is 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
  • brandRules cannot 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 Shipment and AccountingDocument rather than MspStoreOrder / 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.

Questions this guide did not answer?

Ask us. You will get a reply from someone who uses the product every day.

Book a demo Contact us

A 30-minute walkthrough against your own workflow. No slides.