# ShopsWired Developer Documentation — full text > Everything needed to build ShopsWired plugins and themes, concatenated from the canonical Markdown sources. Sections below mirror the pages at https://www.shopswired.com/docs . ================================================================================ SECTION: Native Features · https://www.shopswired.com/features ================================================================================ # ShopsWired — Native Features This is the merchant's guide to everything ShopsWired does out of the box — the features your store gets natively, without installing a single plugin. It's the store-owner counterpart to the developer docs ([Plugins.md](Plugins.md), [Themes.md](Themes.md), [Entities.md](Entities.md), [Recipes.md](Recipes.md)): those describe how to *extend* the platform, this describes what you *already have*. > **Scope.** Everything below ships with every store. Some capabilities are gated > by your plan (higher tiers raise limits) or by your staff role (owner/admin vs. > staff) — those gates are called out where they matter. Features delivered by > optional plugins from the Marketplace (payment gateways, extra widgets, etc.) are > noted as such but are not the focus of this document. --- ## Table of contents - [Products & Catalog](#products--catalog) - [Inventory](#inventory) - [Orders](#orders) - [Fulfillment & Shipping](#fulfillment--shipping) - [Payments](#payments) - [Refunds](#refunds) - [Customers](#customers) - [B2B & Tiered Pricing](#b2b--tiered-pricing) - [Subscriptions & Recurring Billing](#subscriptions--recurring-billing) - [Coupons & Discounts](#coupons--discounts) - [Tax](#tax) - [Storefront & Themes](#storefront--themes) - [Page & Content Builder](#page--content-builder) - [Search, Filtering & Navigation](#search-filtering--navigation) - [SEO](#seo) - [Transactional Emails](#transactional-emails) - [Print Documents](#print-documents) - [Media & File Manager](#media--file-manager) - [Dashboards & Analytics](#dashboards--analytics) - [Notifications](#notifications) - [Custom Records](#custom-records) - [Internal Notes (Memos)](#internal-notes-memos) - [Team, Roles & Permissions](#team-roles--permissions) - [Getting Help & Support Access](#getting-help--support-access) - [Store Settings](#store-settings) - [Account & Security](#account--security) - [AI Assistants](#ai-assistants) - [Multi-Shop](#multi-shop) - [Plans, Billing & Usage](#plans-billing--usage) - [Custom Domains](#custom-domains) - [Wired — Reseller / Dropship Network](#wired--reseller--dropship-network) - [Plugins & Marketplace](#plugins--marketplace) - [Developer Tools](#developer-tools) - [Audit Log & Compliance](#audit-log--compliance) --- ## Products & Catalog Build and sell physical or digital products. - **Core product fields** — Name, SKU, rich description, selling price, compare-at price (shown as a strikethrough "was" price), weight (for shipping), and an active/inactive toggle to publish or hide a product. - **Multiple images** — Attach as many product images as you like, with the ability to override the image shown for a specific variant. - **Options & variants** — Define option dimensions (Size, Color, …) and generate variant combinations. Each variant can carry its own SKU, price, compare-at price, price tiers, stock, back-order policy, images, and attributes — falling back to the product's values wherever you don't override them. - **Digital products & downloads** — Mark a product as digital and attach the files buyers receive. Digital items are delivered automatically once payment clears, and digital-only orders skip shipping and fulfill themselves. - **Custom attributes** — Add named attributes (Material, Origin, …) that can double as storefront filter facets and as conditions for pricing/markup rules. - **Custom fields / metadata** — Store arbitrary extra structured data on a product for your own use or for plugins. - **Tags as collections** — Tags are the native grouping mechanism: they power storefront filtering, coupon targeting, and shipping/pricing/markup rules. (A product's tags act as its categories/collections — there's no separate collection entity to maintain.) - **Tag suggestions** — When you tag a product or target a coupon, the tags already in use are offered as you type, so you reuse an existing tag instead of creating a near-duplicate. Tags are stored exactly as you type them. - **Tags match forgivingly** — Capitalization and punctuation never decide whether a rule fires. A coupon targeting `Men's Shoes` still discounts products tagged `mens shoes`, so a small difference in how a tag was typed can't quietly stop a discount or a markup rule from applying. - **Tag details** — The Tags page (from Products → Tags) lists the tags in active use across your catalog, with how many products carry each one (it favours your most-used tags, so a brand-new or rarely-used tag may take a moment to appear). Optionally give a tag a display name (a tidier or differently-punctuated version of the tag), a description, an image, and a parent tag to group it under. Tags without details work exactly as they always did — this is enrichment, not a list you have to keep up to date. - **Tag landing pages** — Every tag has its own page at `/tag/` showing the products that carry it, with your display name, description and image at the top. However a tag is capitalized or punctuated in a link, shoppers land on the same page. - **Tag pages in search engines** — A tag page is offered to search engines only once you give that tag a **description**, so writing one is what publishes it; described tags are listed in your sitemap automatically. Filtered views of a page (a facet, a sort, a price range) are deliberately kept out of search results, so your catalog is represented by a set of real landing pages rather than thousands of near-identical filtered ones. - **Tag menu** — Optionally add a header menu built from your tags (Theme settings → Navigation → Show Tag Menu). Name it whatever you like — "Tags", "Shop by Category", "Collections". Top-level tags become the menu, and any tag you give a parent nests under it — to as many levels as you set up — so the hierarchy you build on the Tags page becomes your storefront navigation. - **Browse-all-tags page** — A `/tag` page lists your tags as clickable cards with their images and descriptions, and nests sub-tags under their parent to whatever depth you've set up. It's the closest thing to a "collections" landing page, and it's built from the same tag details, so there's nothing separate to maintain. Images are optional everywhere — a tag without one shows its initial instead, so a page with no images still looks intentional. Give it its own heading and intro text under Theme settings → Navigation. - **Custom product URLs** — Give a product a clean, human-readable slug (`/product/summer-tote`). Change it whenever you want — the old URL automatically redirects to the new one so existing links never break. - **Bulk actions** — Select products in the list to publish or unpublish them, add or remove tags across the whole selection, or delete them. Adding a tag leaves products that already carry it untouched, and removing one also strips differently-spelled variants of the same tag, so a selection ends up consistently tagged either way. - **CSV export** — Export your product catalog to CSV. ## Inventory - **Stock tracking** — Track stock at the product level and per variant. - **Back-orders (oversell)** — Allow selling past zero on a per-product or per-variant basis, or leave stock uncapped (unlimited). - **Automatic stock movements** — Stock is reserved and decremented at checkout, and restocked automatically when you refund with restock enabled. Manually built orders only reserve stock once marked paid, so open quotes don't tie up inventory. ## Orders - **Order lifecycle** — Orders move through Created → Processing → Shipped, with Cancelled, Refunded, Partially Refunded, and Payment Failed states. Every order gets a unique, human-readable number (`YYYYMMDD-XXXX`). - **Action-driven status** — Status always reflects what actually happened: it follows from the payment state and explicit actions — Mark as Shipped (with tracking), Cancel (before shipment; a shipped order is refunded instead), Mark Paid, Refund — rather than being hand-picked from a list, so an order can't claim a state that never occurred. Shipping and cancellation each record their timestamp, and full-access staff can undo a mistaken shipment or reopen a cancelled order from the order page. Bulk ship/cancel is available from the orders list. - **Order detail** — Each order captures the customer snapshot, shipping address, line items (with variant and subscription plan), payment details, itemized totals (subtotal, tax, shipping, discount, total), and any applied coupon codes. - **Create orders in admin (POS / quotes)** — Build orders by hand for point-of-sale or request-for-quote workflows: search products, pick a variant and (for subscription products) a plan per line, add line items, calculate totals, and record an internal note. Choose how the customer gets it: **Ship to address**, or **Pickup** at one of your configured pickup locations — a pickup order hides and stores no shipping address, and can then be marked ready for pickup like any other. If your store offers pickup at all, it's the preselected choice here, since a hand-built order is usually a walk-in. Manual orders are flagged, and the staff member who created them is recorded for sales-rep attribution. - **Charge a saved card** — When the order's customer has a card on file, staff can charge it directly from the order instead of recording an offline payment. If the order contains a subscription product, charging it this way also starts the subscription (future renewals bill the same card). - **Editing & bulk actions** — Update status, tracking, and shipping address (address changes are tracked before/after). Apply a status change to up to 200 orders at once. - **Line-item adjustments** — Plugins can add labeled +/- adjustments (card surcharge, gift-wrap fee, wire-transfer discount) that appear on the order. - **Search & filter** — Find orders by email, order number, customer name, or phone. End a name or phone with `*` (e.g. `Jane*`, `555*`) to match everyone whose name/number starts with it (case-sensitive); while a `*` search is active the status filter doesn't apply. Filter by status, date range, customer, and subscription. - **CSV export** — Export orders to CSV. ## Fulfillment & Shipping - **Shipping methods** — Flat rate, weight-based, free shipping, and local pickup; each method can be enabled or disabled, and you arrange them in the order checkout offers them, so the option you'd rather sell sits at the top. - **Rate tiers** — Within a method, price by order amount, by weight, or by both: each tier carries a price and the bounds a cart has to fall inside for that price to apply (order from/to, weight from/to). Leave a bound empty and it doesn't constrain; both maximums are inclusive. Tiers are checked top to bottom and the first match wins, so a narrow tier above a catch-all reads exactly like the rule you'd say out loud — "$0 once the order reaches $100, otherwise $9.95". A cart that matches no tier means the method doesn't apply, and it drops off checkout rather than quoting a fallback price: a small-parcel rate capped at $200 simply isn't offered on a $300 order, leaving your freight method to take it. - **Free shipping thresholds** — Tiers on a *free* method are a condition rather than a price. Give one a minimum order of $100 and the method appears once the cart reaches $100 and is absent below it; a free method with no tiers is always offered. Thresholds are measured on the merchandise subtotal before any discount, so a coupon never retracts free shipping at the last step. - **Shipping zones** — Restrict a method to specific countries and states/ provinces; leave zones empty for worldwide availability. Zones apply to methods that deliver somewhere — a local pickup method is always offered, since the customer comes to you and there's no destination to match. - **Local pickup** — Publish a pickup location (street, city, state, ZIP, country), hours, and instructions shown to the customer at checkout. When a pickup method applies, checkout opens with a **Ship / Pickup** choice: picking up lists your locations (with their details) and skips the shipping address entirely; shipping asks for the address first and lists the shipping methods that address rates to. - **Tax-exempt products** — Mark a product tax exempt and it drops out of the taxable total wherever it's sold: groceries, medicine, gift cards, a service line. The rest of the basket is still taxed normally, and a discount only reduces the tax by the part that reaches taxable items — a coupon limited to exempt products doesn't lower the tax at all, one limited to taxable products comes off in full, and a whole-order discount is shared out by value. Each code in a stack counts on its own, so covering a taxable item in full leaves no tax on it even with another discount applied on top. The exemption is read at the moment tax is calculated, so changing it takes effect on the next order — and each order records which of its lines were exempt, so a receipt or a refund still adds up later. - **Pickup is taxed at the counter** — An order collected in store has no delivery address, so it's taxed where the location is, using the same tax rules (and any tax app) as everything else. Fill in each pickup location's address — at minimum its country — or those orders match no tax rule and go out untaxed. The total updates as the customer switches between shipping and pickup, so what they're shown is what they're charged. - **Tracking** — Attach carrier, tracking number, and tracking URL to an order; the customer gets a shipping-notification email with the tracking link. Name a major carrier (USPS, UPS, FedEx, DHL, Canada Post, Purolator, Royal Mail, Australia Post, OnTrac, DPD, GLS, NZ Post) and you can leave the URL blank — the shipment is tracked through that carrier's standard link, and the order gives you that link to click and check. Choose **Use a custom URL** to override it — for a carrier not on the list, or to point somewhere else entirely. Because a blank URL is worked out fresh every time it's shown, a carrier moving its tracking page doesn't leave your past orders linking into thin air. Whatever the carrier, an order with no tracking link emails the carrier and number on their own. - **Ready for pickup** — A local-pickup order gets its own step before handover: mark it **Ready for Pickup** once it's packed and the customer is emailed that it's waiting, with the location, hours and instructions from the pickup method they chose. Collecting it is then recorded with **Mark as Picked up**, which emails a handover confirmation worded for a collection rather than a shipment ("picked up", no tracking). Both steps are undoable by a full-access user if you jump the gun. - **Auto-fulfill** — Optionally auto-fulfill orders on payment (digital-only orders always auto-fulfill). - **Supplier / dropship fulfillments** — When an order includes products syndicated from another shop (see [Wired](#wired--reseller--dropship-network)), a fulfillment record is created in the supplier's shop with just their items and the ship-to address; the supplier is notified in-app and by email. When the shopper chose store pickup there is no delivery address, so the supplier ships to your pickup location instead — the fulfillment is flagged **ship to store** and its ship-to is your counter, so the supplier can see the parcel is going to a store rather than to a shopper. Supplier packing slips are blind — they brand as your store and never reveal supplier costs. ## Payments - **Gateways via plugins** — Connect one active payment provider per store (Stripe, Square, PayPal, and others) through a gateway plugin from the Marketplace. Each plugin supplies its own webhook setup and, for own-keys gateways, a per-shop webhook URL to paste into the provider dashboard. - **Manual / offline payments** — Record cash, bank transfer, ACH, or any other off-gateway tender directly on an order. "Mark paid" runs the same follow-up as a live charge (receipt, fulfillment, ledger). - **Card vaulting for renewals** — Gateways can securely store a payment method to charge later for subscription renewals and post-trial billing. - **Refresh payment status** — A bank transfer (ACH), Cash App Pay or Afterpay payment takes days to clear, and the order is confirmed automatically when your provider says it has. If that confirmation never reaches your store — the provider dropped it, or it was sent while your store was unreachable — the order sits awaiting a payment that already went through. **Refresh status** asks your provider what it looks like right now and brings the order into line: a cleared payment is confirmed exactly as it would have been, receipt and fulfillment included, and a payment that bounced marks the order payment-failed and returns the stock. It does the same for a **refund still settling**. Most providers accept a refund immediately but finish it later — every Square card refund starts out pending — and that confirmation can go missing in exactly the same way, leaving a refund that shows as pending with nothing to do about it. Refreshing settles it, which is what sends the refund email, restocks and updates the order. The button appears on the Payment card and next to any refund waiting on the provider. It needs full order access and can be used once a minute per order. On a cancelled order it settles refunds only — it will never confirm a payment there. Not every gateway supports it; it's up to the payment plugin. ## Refunds - **Full or partial** — Refund a whole order or specific line items, in one or several installments, each with a reason and an optional restock. - **Gateway or manual** — Process refunds through the payment gateway or record them manually for offline tenders. Safeguards prevent refunding more than the order total or double-refunding, and the order status recalculates to Partially or Fully Refunded automatically. Asynchronous settlement (e.g. ACH) is handled. ## Customers - **Accounts & guest checkout** — Per-store customer accounts with email/password login. Optionally enable passwordless magic-link sign-in, or require an account at checkout to disable guest checkout entirely. - **Sign-up fields** — Decide what the create-account form asks for beyond name and email — phone, company, VAT number, anything you name — from your theme settings (Register → Additional Fields). Type the label a shopper sees, end it with `*` to make it required, and the answers are saved as custom fields on the customer. - **Leads vs. customers** — Contacts captured at guest checkout start as "leads" and are automatically promoted to "customers" when they place an order. - **Addresses & saved cart** — Customers keep multiple saved addresses and a persistent cart tied to their account. - **Custom fields & metadata** — Store extra structured data on any customer. The customer's page has a **Custom fields** editor: whatever they filled in when signing up, plus any name/value you add yourself. A field whose name starts with an underscore (e.g. `_account_no`) is also *searchable*, which is how an import or integration finds that customer again on its next run — so renaming one can break that link. - **Search & filter** — Find customers by exact email, or filter the list by type (customers vs. leads). End a search term with `*` (e.g. `Jane*`) to match every customer whose name starts with it (case-sensitive); while a name search is active the type filter doesn't apply. - **Email preferences** — Each customer chooses what they hear about, per topic: order updates, shipping notices, subscription notices, news & offers, plus a topic for each plugin that emails your shoppers. They manage it from **Account → Email Preferences** on your storefront, or from the unsubscribe link in any email's footer — no login needed. Marketing is off until a shopper opts in (a tickbox on your create-account and checkout forms); account and order-security email is always delivered. Staff can see and change a customer's preferences from the **Email** tab on the customer page, and every change is recorded in the audit log. - **GDPR erasure** — One action anonymizes a customer's personal data and scrubs the PII snapshot across all their orders while keeping the order financials and history intact. (Customers attached to orders are anonymized rather than hard-deleted.) - **CSV export** — Export customers to CSV. - **Log in as customer** — With the right permission (full access to customers), staff open a short storefront session as the customer straight from their admin page — to build a cart, place an order, or add a saved card on their behalf. The session starts from the customer's own cart: whatever the staff member had in their own cart is dropped, not merged in, and it's cleared again when they log out of the assisted session. Orders placed that way are attributed to the staff member, and the action is recorded for audit. - **Card on file** — A customer can save one payment method (via a payment provider that supports it). The saved card bills all of the customer's subscriptions, speeds up checkout (they can pay with it in one click), and can be charged by staff from an order (see Orders). Customers manage their saved card from their storefront account. ## B2B & Tiered Pricing - **Named price levels** — Define levels like *Wholesale* or *Dealer* and set a specific price per product for each. Optionally show the standard price struck through for that level. - **Fallback when a product has no named price** — Choose per level what happens when a product carries no price for it: **require a quote** (the price shows as $0 so a request-a-quote experience can take over — the default), keep the **standard price**, use the **compare-at price**, borrow **another level's price**, or take a **percent / amount off** the standard price. - **Rule waterfall** — Layer conditional rules by product tag or attribute (first match wins) so different product groups price differently per level. - **Assign to customers** — Attach a price level to a customer and their pricing applies automatically everywhere the storefront loads a price — product pages, cards, search grids, and live product grids in the page builder. - **Filter your customer list by level** — Once you define price levels, the customer list shows each customer's level and gains a filter for it, so you can pull up everyone on *Wholesale* — optionally narrowed to customers or leads. Retail customers, the ones with no level, are simply the unfiltered list. - **Quantity price breaks** — Independently of levels, set "buy N or more for a lower unit price" tiers at the product or variant level. ## Subscriptions & Recurring Billing - **Per-product plans** — Offer selectable cadences (weekly, monthly, quarterly, yearly) on a product, each with a label, price, recurring discount, and an optional first-cycle-only discount. Plans can be scoped to specific variants. - **Trials & cycle caps** — Set trial days, a maximum number of billing cycles (or unlimited), and mark a product as subscription-only. - **Billing anchors** — Bill every subscriber on the same calendar day (the 1st of the month, the last day, or a chosen weekday for weekly plans) instead of each customer's signup anniversary. A day later than a short month has bills on that month's last day and returns to the intended day afterwards, so a month-end plan never drifts earlier. For the gap between signup and the first anchored date, choose whether to charge a full cycle up front or nothing at all until that date. - **Amounts that change each cycle** — For things billed by how much actually happened — music lessons, where some months have four and some have five — a plan can be set to let each renewal's amount move within limits you choose: a minimum and maximum quantity, a minimum and maximum unit price, or both. An installed plugin supplies the actual numbers each cycle; the limits are yours, and they are what the customer agrees to when they subscribe. Only a side with a maximum can move, so nothing becomes adjustable by accident, and the limits in force for an existing subscriber are the ones from when they signed up — editing the plan later changes what *new* subscribers agree to. - An amount outside those limits is **refused, not trimmed to fit**: the renewal stops and is flagged for you rather than quietly billing a different number. - A cycle can bill **nothing at all** (set the minimum quantity to 0) — no charge, no invoice, and the schedule simply moves on to the next one. - You can require that a subscription is **never billed without an amount**, for plans where the listed price is only a placeholder. A renewal nothing supplies an amount for is held for review instead of charged. - **See the next charge before it happens** — For a subscription whose amount changes, both you and the customer can see what the next renewal will cost: a panel on the subscription in the admin, and a line on the customer's account page. It is worked out by asking the same plugin the same question the charge will, so what's shown is what gets billed — and if the amount can't be worked out or falls outside the agreed limits, you find out there rather than when the renewal doesn't happen. - **Adjust the limits later** — A subscriber's arrangement can change without cancelling and re-subscribing them: edit a live subscription's quantity and price limits from the admin, or switch per-cycle billing off for that customer entirely. Plugins can't change these limits — only you can, which is what makes them a real limit on what a plugin may charge. - **Contract lifecycle** — Subscriptions run through Active, Trialing, Past Due (dunning), Paused, and Cancelled. Each renewal generates a fresh order/invoice while the contract persists, and the renewal snapshots its own items and totals so later catalog or price changes don't disturb existing subscribers. - **Off-session renewals** — Renewals charge the customer's vaulted payment method; failed charges enter dunning/retry. - **Declines vs. hiccups** — A declined card and a temporary problem on the store's side are handled differently. A decline starts dunning and tells the customer what to fix; a temporary problem is retried quietly on a widening schedule, without dunning emails and without counting against the customer's retry allowance. Either way the reason is recorded on the subscription and shown in the admin, along with the attempt count and when the next attempt is due — and a renewal is never billed twice because an attempt was interrupted. - **Needs-review hold** — If a subscription can't be billed for a reason outside the customer's control — automatic retries exhausted, or a changing amount that couldn't be worked out or fell outside the agreed limits — it is put on hold rather than cancelled: billing stops, the contract stays intact, and the admin flags it for review with the reason and what to do about it. The customer is never cancelled over a problem they couldn't fix. - **Edit a live contract** — Change a running subscription's billing interval, next bill date, cycle cap, and per-line quantities and prices without asking the customer to re-subscribe. Tax and shipping are recalculated from the new lines and address, so the amount always matches what's actually being billed. - **Change history** — Every change to a subscription is recorded with who made it, when, and exactly which fields moved — whether it came from the admin, the customer's account page, or a renewal. A History tab on the subscription shows the trail. - **Admin & customer self-service** — Manage subscriptions from the admin (filter by status, see billing problems on the list and detail pages); customers manage their own from their account (e.g. pause until a date). Shipping is captured at signup and re-priced when the address changes. ## Coupons & Discounts - **Types** — Fixed amount off or percentage off. - **Conditions** — Minimum order amount, maximum total uses (with a live usage counter), a validity window (start/end), and restriction to specific products, product tags, or product attributes. - **Product exclusions** — Take individual products back out of a coupon. Excluded products never get the discount, even when they match the products, tags or attributes the coupon targets — so "20% off everything tagged *sale*, except this one line" is a single coupon rather than a hand-maintained list of everything else. The exclusion holds everywhere the coupon is measured: the cart total, stacked codes, the tax owed, and the offer badge on the product page, which stays off an excluded product instead of advertising a discount checkout would refuse. - **Customer pricing targeting** — Restrict a coupon to specific customer price levels, with a checkbox per tier plus **Regular price** for shoppers on no tier. Leave them all unchecked and the coupon is open to everyone; check some and only those customers can use the code — everyone else is refused it at entry and never sees it advertised. Use it for a trade-only promotion, or for a retail-only sale that shouldn't stack on top of wholesale pricing. - **Attribute targeting** — Restrict a coupon to products carrying an attribute (e.g. `Material`), optionally with a specific value (`Material = Gold`). Add several and an item qualifies if it matches any one of them; leave the value blank to match every value of that attribute. Attributes are matched per line, including the ones the shopper's selected variant adds, so "10% off gold" can discount the gold variant of a ring and skip the silver one. Combined with a product or tag restriction, an item must satisfy both. - **Behavior** — Active toggle; *passive* (auto-applied, and advertised on the storefront); *exclusive* (can't combine with other codes); *featured* (shown in the storefront's promo bar). Multiple codes can stack; adding one that can't be combined is refused as the shopper enters it, naming the code that blocks it, rather than being dropped silently at the end. Stacked discounts can bring an order to zero but never below it. Gift-card and discount-code entry with live validation appears natively at checkout. - **Automatic discounts are advertised, not hidden** — A passive coupon puts an offer tag on the photo of every product it applies to, on product cards and the product page ("60% off"), plus a line under the price saying it's applied automatically at checkout. Where the coupon has an order minimum, the tag says so ("60% off $100+") instead of promising a discount the cart may not reach. The best offer for that product is the one shown. The tag's colours and the wording of the product-page line are settings in the theme's **Promotions** section, where the tags can also be turned off entirely. - **Promo bar** — Featured coupon codes are announced in a bar above the store header, with the code to enter and what it takes off. On by default, and its background and text colours are colour pickers in the theme's **Promotions** settings — recolouring the bar to match a campaign needs no theme edit. ## Tax - **On/off & inclusive pricing** — Enable tax and, if you sell tax-inclusive, flag that prices already include tax. - **Rules by region** — Add rules by country and optional state/province, each with a rate, a display name (Sales Tax, GST, VAT…), and an option to tax the shipping amount too. - **Rates by customer tier** — A rule can be limited to one or more customer pricing tiers, or to "Default" (guests and customers on no tier). Leave a rule unrestricted and it charges everyone. This is how a trade or wholesale tier is handled: give it a rule of its own to charge it a different rate, or leave it off every rule and those customers are charged no tax at all, while retail shoppers at the same address still pay. Where more than one rule could apply, the closer destination wins first (a state rule beats a country-wide one), then a tier-specific rule beats the catch-all for the same region. ## Storefront & Themes - **Default theme** — Every store ships with a clean, complete storefront: homepage, product pages, catalog/search, cart, checkout, account area, order pages, and auth flows — ready to sell with zero setup. - **Theme library** — Switch between multiple ready-made themes (Aura, Onyx, Harvest, Pop, Shift, Timber, and more), each with its own look, homepage layout, colors, and typography. - **Install from the Marketplace** — Browse and install themes; preview a marketplace theme live (desktop/mobile) before installing, and see its README, changelog, and version. - **Brand look & feel** — Upload a logo (reused automatically as favicon and touch icon) and set eight brand colors (primary, hover, background, surface, main/muted text, error, success) that apply across the whole storefront. Clear them to fall back to theme defaults. - **Custom code injection** — Insert your own CSS/HTML/JS at Head Start, Head End, Body Start, and Body End, plus a thank-you-page-only scripts slot for conversion pixels and analytics. These render dynamically, so they can reference live page and order data without editing the theme. - **Catalog / browse-only mode** — Hide "Add to Cart" and/or prices store-wide for a catalog-only site. - **Buy Now button** — Turn on a second button beside Add to Cart that takes the shopper straight to checkout with just that item. It leaves whatever is already in their cart exactly as it was, so a quick single-item purchase never disturbs a basket they're still building. Off by default; set its wording to whatever suits your store. - **Checkout fields** — Decide what checkout asks for beyond name, email and delivery address — phone, PO number, delivery instructions, anything you name — from your theme settings (Checkout → Additional Fields). Type the label a shopper sees, end it with `*` to make it required, and the answers are saved with the order: shown beside the customer on the order page and printed on the packing slip. Phone is asked for by default; remove it if you don't need it, or mark it required if you do. - **Focused checkout** — On by default: the checkout page is stripped down to the order — no navigation, search, announcement bar or full footer, just your store name, a link back to the cart, and the policy links you choose (Terms, Privacy, Contact…), so nothing competes with completing the purchase. Turn it off to show your full site header and footer on checkout. - **Edit theme files** — A built-in code/file editor lets you customize any theme file, adjust theme settings, and manage versions, clones, and updates. - **Storefront stays up through a hiccup** — Your public pages are served from a global cache close to the shopper. If the store can't render a page for a moment, the last good version of it is served instead of an error, so shoppers browsing your catalog keep browsing. Cart and checkout are always live, never cached, so nothing a shopper is in the middle of is ever served from a copy. When there's no copy to fall back on — a live page like the cart, or one nobody has visited recently — the shopper gets a clean page in your store's name explaining that it will be back shortly, and it retries on its own, rather than a raw server error. ## Page & Content Builder - **Visual layout builder** — Compose pages from rows → columns → blocks with drag-and-drop, no code. Block types include heading, text, rich text, raw HTML, image (with link), button, live product grid (honoring B2B/customer pricing), and plugin-provided blocks. Rows control width, padding, gap, and background. - **Homepage & announcement bar** — The homepage is built entirely in the layout builder; an optional header layout renders an announcement/promo bar above the nav. - **CMS content pages** — Create custom pages (About, Contact, policies, …) served at `/page/{slug}` and auto-linked into navigation. **Clone** in a page's ⋮ menu opens a new page pre-filled from that one, so a family of pages that share a layout is built once and copied; give the copy its own path before saving. - **Navigation menus** — Build and reorder header, footer, and account menus, including nested dropdown items. ## Search, Filtering & Navigation - **Admin menu search** — A search box at the top of the admin sidebar filters every place you can go — sections, dashboards, custom records, and pages added by plugins — down to a flat list you can arrow through and open with Enter. Ctrl/+K jumps to it from anywhere in the admin. You only ever see what your role can reach. - **Pin what you use** — The 📌 on any sidebar entry pins it to a Pinned block at the top of the menu. Pins are per store and per browser, so each person on your team keeps their own shortlist. - **Product search** — A header search bar plus a full catalog/search page with keyword relevance. Owners can re-index the catalog on demand and choose the active search provider (built-in, or one supplied by a plugin). - **Faceted filtering** — Filters are generated automatically from product attributes, with value counts, collapsible groups, a merchant-controlled display order, active-filter pills, and clear-all. An optional min/max price filter can be toggled on. - **Sorting & pagination** — Sort by relevance, newest, oldest, price, or name; set the default sort, and switch off any sort options you don't want shoppers to see (the Sort By control hides itself when fewer than two are left). "View More" pagination with a configurable page size (12/24/48) and contained or full-width layout. Filters collapse into a mobile drawer on small screens. ## SEO - **Meta & social tags** — Automatic page titles (with your store-name suffix), meta descriptions, canonical URLs, Open Graph tags, and Twitter cards. - **Structured data** — JSON-LD output per page. - **Robots control** — Per-page `noindex, nofollow` support. - **Clean URLs with redirects** — Custom product slugs with automatic 301 redirects when a slug changes; products feed the sitemap. - **Fast images** — On-the-fly resizing and WebP conversion (with automatic fallback for older browsers) and responsive size buckets for faster pages. ## Transactional Emails - **Themeable customer emails** — One store-level email template drives all customer-facing emails, with a built-in default if you don't customize it. - **Covered messages** — Order confirmation, order received (see below), shipping notification (carrier + tracking link), order cancelled, order refunded, order partially refunded, digital-download delivery, subscription payment failure (with the retry date and a link to update the card), account welcome, magic-link sign-in, and customer password reset. - **An order that isn't paid yet still gets acknowledged** — The order confirmation is a receipt, so it is only ever sent once payment completes. When a shopper checks out with a slower payment method that settles over the next few days, or lands on the thank-you page without finishing payment at all, they get an "order received" email instead: it names the order, says plainly that it is not confirmed until payment clears, and — where there is still something for them to do — links them to the page where they can complete it. The confirmation follows on its own once the money arrives, so a shopper is never left holding a receipt for an order that was never paid, nor a silent thank-you page. - **Your own subject lines** — Every customer-facing email's subject is editable in the theme editor alongside the message itself, so "Order Confirmation - #1234" can become whatever your store sounds like. Subjects can include order and store details, and can change wording based on the order — one subject for a parcel that shipped and another for one collected in store. Leave a subject blank and the built-in one is used, so there is nothing to set up before your emails work. - **Every message has a topic** — Each customer email belongs to a topic the shopper can unsubscribe from on its own, so someone can stop your newsletter without losing their order confirmations. Unsubscribe and preference links are added to the footer for you; plugins that email your customers declare their own topics, which appear alongside yours. - **Staff email alert preferences** — Each staff member chooses which notification categories email them, and can mute all emails per store. - **No one customer can be flooded** — Separately from your store's own limits, each shopper has a ceiling on how much mail they can receive from you in an hour and in a day. It sits far above what a real customer gets (their order confirmations, shipping notices and downloads, several orders over), and it is the same on every plan — a bigger plan buys more reach, never more of one person's inbox. If a plugin ever loops on one address, the messages are dropped and you're told which customer it was, instead of them being buried and unsubscribing. - **Sending limits** — Transactional email (orders, shipping, account) has **no monthly budget** — it's meant to be sent. There is only a sending *rate* per plan, an hourly and a daily ceiling set well above a busy day, so a misbehaving plugin can't quietly mail your whole customer list overnight; sign-in links and password resets are never limited at all. **Marketing email is separate and deliberately small** — a token monthly allowance per plan, and none on Free — because the built-in transport is built for receipts, not campaigns. Install a marketing plugin to send at volume: it delivers through its own provider and its own capacity, and those sends don't touch your allowance. Settings → Usage shows marketing for the month and transactional for the day, and you're notified the first time a message is dropped rather than after the fact. ## Print Documents - **Packing slips** — Print-ready packing slips for orders and supplier fulfillments, showing ship-to and customer info, items with SKU/options and quantities, totals (blankable for gift orders), tracking numbers, and order notes. Supplier slips are blind (your branding, no supplier costs). ## Media & File Manager - **Per-store file storage** — Organized into roots (product images, downloads, theme assets) plus a "browse all files" view for owners/admins. - **Operations** — Browse folders, upload, download, delete, move, and create folders, with automatic storage metering. Bulk actions let you copy a file's URL or Liquid reference, delete, or move many files at once. - **Refresh a replaced file** — Public files are cached close to your shoppers for a long time, so re-uploading one under the same name can keep showing the old version. Select it and clear its cached copies — every resized and reformatted variant goes at once, up to 30 files per refresh. Shoppers who already loaded the file keep their copy until it expires on their device, so when a swap has to be visible immediately, upload under a new name instead. ## Dashboards & Analytics - **Customizable dashboards** — Build one or more dashboards by dragging widgets from a gallery, with a selectable reporting period. Star one as your default landing dashboard. - **Personal vs. shared** — A dashboard can be personal (only you) or shared with the whole team (owner/admin edit the shared ones). - **Built-in widgets** — Revenue, Orders, Average Order Value, Customers, Revenue Over Time (chart), Top Products, and Order Status breakdown. - **Plugin widgets** — Installed plugins can add their own dashboard widgets from the same gallery. ## Notifications - **In-app notifications** — A shared, read/unread notification feed with an unread badge, spanning order, subscription, system, billing, plugin, and fulfillment categories (plus plugin-defined categories) at info/success/warning/error severities, each with a deep link to the relevant record. - **Marketplace alerts** — If you sell plugins or themes, a notification is raised when a payout reaches your connected Stripe account, and when one can't be sent (deep-linked to your developer settings). - **Subscription alerts** — When a subscription is paused, resumed, or cancelled, a notification is raised (deep-linked to the subscription). A new signup isn't notified separately — its order already appears as a new-order notification. - **Email delivery** — Notifications email each staff member per their preferences; owners/admins are opted in by default for order, subscription, billing, plugin, and fulfillment alerts. ## Custom Records - **Merchant/plugin data models** — Store custom data models (product reviews, lookup tables, plugin entities) with dynamic fields, unique-value enforcement, indexing, and filtered/sorted/paginated queries. Managed from a dedicated admin area, and memos can attach to any record. ## Internal Notes (Memos) - **Attach notes to anything** — Add internal, staff-only notes to orders, products, customers, coupons, and custom records. Each memo is stamped with its author, multiple memos per record are supported, and they're cleaned up with the parent. Memos are never shown on the storefront. ## Team, Roles & Permissions - **Invite staff** — Invite users by email (an invite is emailed and a pending account created if they're new), assign roles, resend invites, change roles, and remove users. *(Owner/admin.)* - **Built-in roles** — **Owner** and **Admin** have full access; **Staff** is scoped (write orders/customers, full products/coupons/records, read reports/ subscriptions, write fulfillments/memos; no theme, plugin or settings access). - **Custom roles** — Owners can build scoped roles (e.g. a Sales Rep who can create orders but not refund) by setting per-resource capability levels — None / Read / Write / Full across orders, customers, products, coupons, reports, records, subscriptions, fulfillments, themes, plugins, and memos — plus a **Log in as customer** (impersonation) toggle, and bundle plugin permissions into a role. - **Plugin access is graded by consequence** — For plugins the levels track damage rather than editing: *Read* views a plugin's settings, logs, files and version history; *Write* configures and runs a plugin that's already installed (change its settings, turn it on or off, run its actions); *Full* adds installing and uninstalling, editing plugin code, managing plugin credentials, changing a plugin's plan, and publishing to the marketplace. So you can give someone "keeps our plugins working" without letting them install a paid plugin or rewrite one. - **Log in as customer (impersonation)** — A dedicated permission, separate from the customers capability, so a role can hold it *without* destructive customer access (e.g. a Sales Rep who can impersonate but can't delete or anonymize customers). Owner and Admin have it by default. - **Per-user plugin permissions** — Grant plugin-defined permissions (like "manage pricing") to individual users. ## Getting Help & Support Access - **Support page** — A dedicated section in your store admin with every way to reach us: live chat, email, and the documentation. *(Owner/admin.)* Start a chat and it follows you around your admin, so you can carry on working while you wait for a reply. - **Live chat is a Business/Enterprise benefit** — Chat comes with the Business and Enterprise plans. On other plans the Support page points you to email and the documentation instead, with a link through to your plans. - **Chat hours** — We're on live chat Monday to Friday, 9am–5pm Central. Outside those hours the page points you to email instead of opening a chat nobody's there to answer. - **Support codes** — Generate a one-time code and give it to whoever is helping you — a support agent, a developer, a friend who knows the platform. When they enter it, they get temporary access to your store without you sharing a password or adding a permanent user. Choose how long it lasts: 1 hour, 24 hours, 3 days or 7 days. - **What a support session can do** — Read your settings (general, theme, customer, shipping, tax, payments, search, usage) so the usual "my checkout isn't working" questions can actually be answered, and work on day-to-day data: orders, customers, products, coupons, records, fulfillments and memos. It can also edit your theme and configure your plugins — change a plugin's settings, turn one on or off, run its actions — because that's usually the whole reason you handed out the code. **What it can't do:** change any setting, issue refunds, delete or anonymize anything, export or import your data, log in as one of your customers, touch billing or your plan, see or change your team, or — on the plugin side — install or uninstall a plugin, edit plugin code, or see or change plugin credentials. Support access can't be granted by assigning a role — only by a code you generate. - **Always reversible** — The Support page lists everyone currently holding access and when it ends. End any session immediately with one click, and every session expires on its own regardless. Granting and revoking are both recorded in your audit log. - **Codes expire fast** — A generated code is redeemable once, within 30 minutes. If nobody uses it in time, it simply stops working — generate another. - **Entering a code** — Anyone with a ShopsWired account can redeem a code from **Enter Support Code** in their account menu; the store it opens becomes available in their store switcher for the duration, with a banner making it clear they're working in someone else's store. ## Store Settings - **Store identity** — Name, slogan, currency, subdomain, and custom domains, all from Settings → General. - **Timezone** — Set your store's own clock (e.g. *America/New York*). Your store keeps its records in universal time, so a timezone is what lets "today", your opening hours, and anything scheduled mean what you'd mean by them rather than what they'd mean in London. Plugins that deal in dates — schedules, bookings, reports, business hours — follow it automatically, so you set it once. Leave it unset and your store runs on UTC. - **Import & export** — Settings → Import/Export moves your records in and out as CSV: pick the entity (products, customers, orders, custom records…), export what you have, or upload a file to update existing rows and add new ones. Rows are matched by their `id` column, and a template download gets you the right headers. - **Don't run plugin hooks for this import** — an option on the import. Normally an import is a change your plugins should see, so anything that syncs, notifies or calculates from those records reacts to it, row by row. On a large file that is the slow part, and a strict plugin can reject rows you are deliberately correcting. Turning it off makes the import a quiet data load: plugins won't see these rows until the next time something changes them. The choice is recorded in the audit log alongside the import. ## Account & Security - **Profile & password** — Update your name and email; change your password. - **Two-factor authentication** — TOTP authenticator-app 2FA (QR/secret enroll). - **Passkeys** — Add passkeys (Touch ID / Face ID / security keys) for passwordless sign-in. ## AI Assistants Connect an AI assistant to your account and let it do the work you'd otherwise do in the admin: set up a store, build the catalog, adjust settings, browse and install plugins and themes, configure them, and edit your theme. - **Connect from anywhere** — Under **Account → Assistants**, pick a store and copy its connection URL. Paste it into an assistant that supports connectors and it sends you back to ShopsWired to sign in and approve. For an assistant running on your own machine, create an access token instead and paste that. - **Choose the stores** — Pick which of your stores a connection covers. Pointing it at a single store also gives the assistant that store's own plugin tools. - **Choose the access level** — **Restricted** runs the store: products, orders, customers, coupons, settings, theme editing, and installing and configuring plugins. **Full** adds building and editing plugins, uninstalling plugins and themes, deleting records and importing in bulk. Full access needs you to be the owner or an admin of every store it covers. - **It reads the listing before it installs** — Ask for a plugin or theme and the assistant can search the marketplace by name, description, author or tag, then read the publisher's own documentation — setup steps, required settings, and any other plugins it depends on — before anything is installed. It writes your theme and plugin code against the same published reference the developer docs serve, rather than from memory. - **Always off limits** — At either level, an assistant can never issue a refund, charge a saved card, buy a paid plugin, change billing or your plan, close a store, change your team or roles, read plugin secrets, or log in as a customer. Those stay with a person in the admin. - **Never more than you** — A connection acts as you, and only within your own permissions on each store. Staff access produces a staff-level assistant. - **See everything it did** — Every change an assistant makes is recorded in that store's [audit log](#audit-log--compliance), marked as an assistant action and separate from your own. - **Its edits are your edits** — An assistant editing a theme or plugin writes to the same place the admin's file editor does, so its work shows up there and yours shows up to it. It can also check what changed since it last looked, and fold a finished set of edits into the theme or plugin itself. - **It can work with your files** — An assistant can add files to your [File Manager](#media--file-manager), read them back, move or rename them, and create folders, naming the area each file goes in: public, private, theme or plugin assets, or a folder of its own for working data. Large files go straight into your storage rather than through the conversation, and it can produce a short-lived link to download one. Deleting a file takes full access, since there's no undo. Each change appears in the audit log. - **It can run a script and read the result** — With full access, an assistant can run one of a plugin's scripts and get back what it printed and any error it hit, the same as pressing **Run** in the file editor. So it can try a change, see what happened, and fix it without you relaying the log — and it can do a one-off job the same way. Longer work it queues instead and reads from the plugin's log. - **Disconnect at any time** — Disconnecting from **Account → Assistants** takes effect immediately and removes the entry; the connection's next request is refused. What it did stays in the audit log. Changing your password disconnects everything. - **Plugins can add their own tools** — A plugin can publish tools of its own to a connected assistant, so "how much stock is left on ABC-1" or "sync the warehouse" becomes something you can just ask for. What an installed plugin offers is listed under **Assistant Tools** on its capabilities page, each with the description the assistant reads; they work while the plugin is active. ### Assistants your shoppers bring Separately from the assistant *you* connect, your storefront answers the AI assistants your **shoppers** use. A shopper asks theirs to find something, and it can search your catalog, look up a product properly and hand the shopper a link straight to checkout — the same way a person browsing your site would, without you setting anything up. - **On by default, yours to switch off** — Under **Settings → General → AI Shopping Assistants**. It's on because it works the way your storefront already does: an assistant sees the products, prices and pages any visitor can see. - **What it can do** — Search your catalog (and narrow by tag, price or availability), read a product's full details including options, variants and what's in stock, look up your store's currency, delivery options and page links, and turn a chosen basket into one checkout link. - **What it cannot do** — Everything else. It can't see orders, customers or anything about a shopper, it can't change a single thing in your store, and it can't buy: the link it produces opens your normal checkout, where the shopper enters their own address and pays themselves. Nothing is charged without a person completing it. - **Prices are always yours** — The checkout link carries products, quantities and chosen options — never a price. Every amount is worked out by your store when the link is built and again when the shopper opens it, so an assistant quoting a stale or invented price cannot make it real. - **Tell them what your pages don't** — A short note ("we only ship within the EU", "sizes run small — suggest one up") is read by an assistant before it answers a shopper. It's the fastest way to stop one guessing about you. - **Your plugins can join in** — Installed plugins can offer these assistants tools of their own, so "which branch has it in stock" can be answered from your own systems. You can switch plugin tools off on their own while keeping catalog search on. ## Multi-Shop - **Own multiple shops** — A single account can own or belong to multiple stores, each with fully isolated data, and switch the active store from the admin shell. Billing and account settings span all of your stores. - **Create & close shops** — Create new stores at any time. Owners can close a store from the Danger Zone: export Products/Orders/Customers to CSV first, then a step-up confirmation schedules permanent deletion after a **7-day recovery window** you can abort. ## Plans, Billing & Usage - **Plan tiers** — **Starter** ($29/mo), **Pro** ($99/mo), **Business** ($249/mo), **Enterprise** ($499/mo), and **Dedicated** (custom, single-tenant, managed by our team). Starter runs a full store on ready-made themes and plugins; Pro adds building your own plugins, wholesale/B2B pricing, and the Wired supplier network. Higher tiers raise request throughput, background-task capacity, storage quotas, scheduled-job frequency, and data-retention windows. - **Billing accounts** — Create multiple named billing accounts (e.g. "Personal", "Client ABC"), manage cards through the Stripe customer portal, transfer an account to another user, and assign each store to a billing account. - **Plan management** — Subscribe, switch (with proration), or cancel a plan (access retained until period end); handles card setup and 3-D Secure. - **Usage view** — See your current plan, storage usage (records and files) versus limits with projected overage, container-compute spend versus your monthly cap, and your plan's rate/retention limits. ## Custom Domains - **Serve from your own domain** — Add a custom domain and verify ownership via CNAME + TXT records; see Verified/Pending status and remove domains. Use a subdomain such as `www.example.com` or `shop.example.com` — root domains (`example.com`) aren't supported yet, since DNS doesn't allow the required CNAME record on a root domain. Your store keeps its `.shopswired.com` subdomain too, and customer-facing links (emails, packing slips) automatically use your public domain. - **Discoverability** — Optionally list your store in the Wired Marketplace. ## Wired — Reseller / Dropship Network A native supplier ↔ reseller network that links ShopsWired stores so you can resell another shop's products (or let others resell yours). *(Owner/admin; staff see fulfillments.)* - **Outbound & inbound links** — Wire to suppliers whose products you resell, and see resellers who sell yours. Discover and request suppliers (including by subdomain), and approve/reject inbound requests. - **Who controls what** — Each shop owns its own half of a link. As the supplier you approve the link and set the **offer scope**: which products a reseller may carry, and what you charge them for each. As the reseller you pick which of those products to sync and add your markup on top. Neither shop can change the other's half, so a supplier can't re-price your storefront and a reseller can't reach beyond what you've offered or change what they owe you. - **Offer scope** — Rules that decide which of your products a reseller may carry and at which of your prices they're billed. A rule can require a tag and any number of attributes ("tagged rugs, Brand Acme, Material Wool"), and a product qualifies if **any** rule matches, so several rules read as alternatives — each with its own billed price, so you can offer rugs at wholesale and lamps at dealer on one link. No rules offers your whole catalog at your selling price. - **Markup rules** — Add conditional markup by tag or attribute — percent, fixed amount, or "request for price". Markup is added to the price the supplier bills you, so the difference is yours and what you owe never includes it. A product no rule matches sells at the billed price with nothing added, so nothing you carry is ever priced below what it costs you. - **Fulfillments & ledger** — Wired orders create supplier fulfillments automatically, and a financial ledger tracks balances across your wired relationships and payouts (Stripe Connect). ## Plugins & Marketplace - **Browse & install** — A built-in marketplace with Featured, Themes, and Plugins sections, search, and per-item detail (readme, versions, pricing). Searching shows its own results view with a Clear button that returns you to browsing. Free items install directly; paid items support one-time purchase, monthly subscription, or tiered plans. The browse section, search term, open listing, and author view all appear in the page address, so any view you reach is a link you can bookmark or share with a teammate. - **Manage installed plugins** — Activate/deactivate, uninstall, install from a `.zip`, clone, check for and apply updates, and switch versions. **Deactivating a monthly plugin does not end its charge** — it stays on your invoice until you uninstall it or move it to a $0 tier. - **Plugin settings** — Each plugin exposes a settings form, optional secrets, a storage browser, and in-app purchase products. You can run a plugin's scheduled jobs on demand and trigger its manual actions, with output in a logs view. - **In-app purchases** — Plugins can sell you one-time unlocks or consumable credits after install; you're charged through Stripe and the developer is paid a share. ## Developer Tools For merchants who build their own plugins/themes or work with a developer. - **Developer mode & CLI** — Enable developer mode to use the `shopswired` CLI for local plugin/theme development (bypasses caching on your preview subdomain), and authorize the CLI with a one-time code. - **Container jobs** — On paid plans, opt in to let plugins run isolated container jobs (image/PDF/OCR/scraping/builds) with per-size pricing and an owner-set monthly compute budget cap. - **Payment webhooks** — A per-shop payment webhook URL to register with your gateway so refunds and async payment updates sync back. - **Log level & asset cache** — Set script log verbosity and bust the edge asset cache to force fresh theme/plugin assets. - **Errors by kind** — A second view of the log, alongside the stream, where your plugins' errors are grouped by what actually went wrong, most frequent first — so an error happening four hundred times reads differently from one that happened once. Each row shows how often it has occurred, when it started and last happened, its spread across the hours of the day, and a **NEW** marker when it wasn't happening in the previous period, which is usually the row worth looking at. Values that change between occurrences, like order numbers and web addresses, are shown as placeholders so that repeats of the same problem land on one row; the most recent full message sits underneath it, and **Find** jumps to the matching entries in the stream. Choose today, the last 7 days, or the last 30. The counts stay accurate even when a fast-repeating error is only sampled in the stream itself, and this history is kept far longer than the individual log entries are, so you can still tell when something started after the entries behind it have aged out. - **Acknowledging an error** — Once you've seen an error and know what it is, **Ack** takes it out of the list so what's left is what you haven't dealt with. Nothing is deleted: it keeps counting, it's one click away under **Show acknowledged**, and **Restore** puts it back. An acknowledgement lasts until the plugin behind the error releases a new version, at which point the row returns on its own — the version you judged is no longer the version you're running, whether the fix worked or something brought the problem back. - **Pages that change by country** — Storefront pages are stored and reused, so the same page is served to everyone who asks for it. If your theme shows something different depending on where the shopper is, turn this on and each country gets its own stored copy; leave it off otherwise, since it multiplies the stored copies of every page by the number of countries you sell to. Cart, checkout and account pages are unaffected — they are never stored. Switching it either way clears the pages already stored, so the change takes effect right away. - **Automatic abuse protection** — Every store is protected against clients that request pages far faster than a person can read them: scrapers, misconfigured crawlers, and audit tools left on a high thread count. Real shoppers are nowhere near the threshold — signed-in customers are measured individually, so a whole office shopping from one connection is never treated as one visitor — and checkout is held to a separate, far higher threshold that ordinary browsing can never use up, so a customer paying is not turned away no matter how busy the rest of the store is. Verified search engines are exempt, so indexing is unaffected. Search and filtering get the opposite treatment: they are the heaviest pages a store serves and the first thing automated traffic goes after, so they are held to their own, lower threshold. A flood of searches can no longer use up what the rest of the store needs — shoppers reading product pages keep browsing normally while the search traffic is turned away. Alongside this, each store has an overall rate that comes with its plan, shown under Usage; checkout has its own generous allowance there too, and search its own smaller one. A client that is asked to slow down gets a plain "too many requests" page and can carry on a few seconds later; nothing is blocked for longer than that. Nothing to configure; contact support if a store has a legitimate reason to need more. - **When the store is at capacity** — Plugins normally wait their turn when the store is busy, so a busy moment costs a little speed and nothing else. If a request runs out of time waiting, you choose what happens: **serve anyway** (the default — the page goes through without that plugin's work, and the plugin is named in its log), or **fail the request** so nothing completes half-done. Serve anyway keeps the store up; fail is the right choice when a plugin affects prices, tax or stock, where quietly skipping it is worse than a visible error. **Checkout is protected either way.** While an order is being priced, plugins that set tax, shipping, discounts or prices wait much longer than anywhere else, and if one still cannot run the checkout stops with a message telling the shopper nothing was charged — rather than quietly taking payment for an order priced without it. This does not depend on the setting above, and it applies before any money moves, so a stopped checkout costs the shopper a retry and the merchant nothing. - **Sell on the Marketplace** — Connect a Stripe account to sell paid plugins and themes (platform fee applies). - **Submit, withdraw, and resubmit versions** — Submitting sends the version you currently have installed for review. While it is still awaiting a decision you can withdraw it, and a withdrawn or rejected version can be submitted again under the same version number once you've made changes — a number is only used up once the version has been approved, because from that point other stores can install it and it has to keep working exactly as it did. Only then do you need to bump the version in `manifest.json` to submit an update. - **Earnings** — Track what your paid plugins and themes have earned: each sale lists the item, date, amount, and whether it has reached your Stripe account. Paid amounts arrive in your Stripe balance and then follow your own Stripe payout schedule to your bank. Anything still pending is money you're owed — it is re-sent, never dropped. ## Audit Log & Compliance - **Audit trail** — An automatic log of create/update/delete actions across products, orders, customers, coupons, custom records, and settings. Each entry records the actor (user, customer, system, script, plugin, or a connected [AI assistant](#ai-assistants)), IP address, and before/after changes (secrets are never exposed). - **Filter & retain** — Filter by action, entity, actor, IP, or plugin; older entries are trimmed per your plan's retention window. Filtering by actor type answers "did someone outside my team do this?" — pick **mcp** for changes made by a connected [AI assistant](#ai-assistants) or **support** for a support session. Surfaced under the reports capability. - **Every email your store sent** — Customer-facing mail is logged like any other action, against the record it concerns, so an order's history reads "shipped" and then "shipping notification sent" in one list. Each entry names the recipient, the subject and the message type, and a send that failed is recorded with the reason — so "the customer says they never got it" has an answer. Account-level mail from ShopsWired itself (sign-in and shop-closure notices) is not logged here. - **See what was actually sent** — Open **Preview message** on a logged email to read the exact message the customer received, in HTML or plain text. Remote images are held back until you ask for them, so opening a preview doesn't fire the tracking pixels in your own marketing mail. Message bodies are kept for a shorter period than the log entries themselves; after that the entry still records the delivery, and the preview says the message is no longer stored. - **Where they're kept** — Recorded messages live in your file manager under `_audit/{date}/email/` and count toward your storage, so you can see what they take up. The folder is read-only: it's a record of what your store sent, so it can be browsed and downloaded but not edited, moved or deleted. ================================================================================ SECTION: Plugin API Reference · https://www.shopswired.com/plugins ================================================================================ # ShopsWired Plugin Development Guide Plugins in ShopsWired are written in modern JavaScript (ES2015+) and run server-side on **ShopScript** — ShopsWired's custom synchronous JavaScript runtime that powers every customization script (plugin hooks, routes, widgets, scheduled jobs). Use `const`/`let`, arrow functions, template literals, destructuring, optional chaining, and `for…of` freely — the examples in this guide do. Plugins can hook into storefront events, modify data, schedule tasks, define routes, split code across files with `require()`, and interact with the database via the provided Bridges. > **Looking for a "how do I build X?" walkthrough?** This guide is the reference > for individual surfaces (bridges, hooks, routes, widgets). Goal-oriented, > end-to-end recipes that compose them — order attribution, sales-rep assisted > carts, bulk CSV import, unpaid-order recovery — live in [Recipes.md](Recipes.md). ## Where your plugin lives, and how to work on it A plugin installed on a store is a **package** — the whole directory, uploaded as one — plus any **edits made on the store since**, from the admin's file editor or from an AI assistant a merchant has connected. Edits sit on top of the package: the store runs the edited copy of a file and the package copy of everything else. That matters as soon as more than one person (or one assistant) touches a plugin, so the CLI treats it the way you'd expect a version-control tool to. ```bash shopswired login # authenticate (opens the browser, no password in the terminal) shopswired link # pick the store to develop against shopswired init --type plugin --id my-plugin # scaffold a new one shopswired pull --type plugin --id my-plugin # bring the store's current copy down shopswired push --type plugin --id my-plugin # send this directory up as the package shopswired dev --id my-plugin # live development (below) ``` **`pull` gives you what the store is actually running** — the package with the store's edits already applied, as plain files. It's the honest starting point whether you're picking up someone else's plugin or catching up on what an assistant changed. **`push` replaces the package with your directory and clears the store's edits.** That's what makes a push mean something: without it, a file edited on the store would keep shadowing the one you just pushed, and your push would appear to succeed while changing nothing. Because that discards work, push checks first — exactly like pushing to a shared branch: ```text ✗ 2 file(s) changed on the store since this directory last synced: edited scripts/hooks.js added tools/report.js These were edited in the admin or by an assistant. Pushing now would replace them. Run 'shopswired pull --type plugin --id my-plugin' to bring them in, or re-run with --force to discard them. ``` Pull, look at what changed, push. Or `--force` when you know the store's copy is the one to drop. The comparison is against what this directory last synced (a `.shopswired-sync.json` written by pull and push — add it to `.gitignore`), so your own local edits are never mistaken for someone else's; only changes made *on the store* stop a push. A directory that has never synced — an existing checkout, or a fresh clone of the repo you've been pushing from all along — is not treated as suspect. The store still knows which files are edits on top of its installed package, and those are the only ones a push would destroy: if there are none, the push goes through as it always did. **Working entirely on the store.** You don't have to use the CLI at all. The admin's file editor and a connected assistant both edit the store directly, and the change is live immediately — no push, no install. What they leave behind is that layer of edits, marked `MOD` in the file editor. When a set of changes is finished, fold the layer into the plugin itself: the file editor offers a 📦 action while a plugin or theme has edits, and an assistant has the same one. The plugin keeps running exactly as it was; it simply stops being a package plus a pile of edits and becomes one thing again — downloadable, publishable, and safe from being shadowed by a later install of the same plugin. A `pull` afterwards gives you those files with nothing layered on top, which is how store-side work gets into a git repo. This applies only to a plugin or theme the store owns. A marketplace install keeps its edits as a layer on purpose — that layer is exactly what survives the publisher's next update. Clone it first if you want to develop your own copy. **Two workflows, and you can use both:** - **Live development** — `shopswired dev` watches your directory and the store runs your local files directly, saving on each change with no push and no install. Nothing is written to the store, so ending the session puts it straight back to its installed copy. This is the fast loop for writing and debugging. - **Keeping a repository** — `pull` into a git repo, commit, work locally, `push` when you want the store to have it. The package is one directory of plain files, so it diffs and reviews like any other source tree. If a merchant or an assistant edits the plugin on the store, the next `pull` brings it into the repo as a normal commit. > Themes work identically — same commands with `--type theme`. ## Directory Structure A plugin is one directory of plain files, named however you like — the `id` in its `manifest.json` is what identifies it to the store, not the folder name. Every path inside the plugin (scripts, `require()`, templates, assets) resolves from that directory's root. A typical plugin looks like this: ```text my-plugin/ ├── manifest.json ├── hooks.js # Contains event handlers ├── render.js # Storefront rendering hooks ├── bridges.js # Bridge scripts ├── lib/ │ └── feed.js # Helper module pulled in with require('./lib/feed') └── test/ └── feed.js # Tests — see "Testing your plugin"; never published ``` ### Splitting code across files (`require`) Plugins can split their JavaScript across multiple files using Node-style `require()`. Paths resolve **relative to the requiring file** and are sandboxed to the plugin's own directory — absolute paths and `../` escapes are rejected, and there is no `node_modules` resolution (the engine ships no npm packages). ```javascript // cron.js const feed = require('./lib/feed'); module.exports.run = function (ctx) { const items = feed.fetchLatest(); console.log('fetched', items.length, 'items'); }; ``` ```javascript // lib/feed.js module.exports.fetchLatest = function () { return fetch('https://api.example.com/feed').json(); }; ``` Compiled modules are cached **per plugin** and re-used across runs — `require('./lib/feed')` parses and compiles `lib/feed.js` exactly once until the source changes. Saving a file override (or reinstalling the plugin) invalidates the cache automatically, so the next run picks up your edit. `require()` is available in every script: scheduled scripts, durable bg tasks (`sw.task.bg`), route handlers, widget scripts, hook scripts, `sw.task.run` runs, and storefront render scripts. On the render path, keep the module tree small — see [Template Render Hooks](#template-render-hooks). ## The `manifest.json` The manifest defines the plugin identity, the scripts to execute, and configurable settings. ```json { "id": "my_plugin", "name": "My Plugin", "version": "1.0.0", "scripts": [ { "path": "hooks.js" }, { "path": "render.js", "routes": ["/product/*", "/"] }, { "path": "cron.js", "schedule": "* * * * *" } ], "settings": [ { "key": "api_key", "type": "text", "label": "API Key" } ] } ``` - `routes`: (Optional) Restricts a script to only run on matching storefront routes. - `templates`: (Optional) Restricts a script to only run when rendering specific template names. - `dataloaders`: (Optional) Restricts a script to only run on pages using specific data loaders (e.g. `["checkout"]`, `["product"]`, `["cart"]`). > **Scoping a script makes the page faster, not just quieter.** On a page a script's > `routes`/`dataloaders` exclude, the script is skipped entirely — its top-level code > never runs there, so anything it does at load time (building a lookup table, reading > settings) happens only on the pages it declared. Scope your storefront scripts and the > rest of the store stops paying for them. Two things keep loading regardless, by design: > a script that exports any `filter.*` (filters are available to every page's Liquid, so > they can't be route-scoped), and a script that declares `templates` (the template can > still change while the page renders). A script whose exports are all non-render — order > and product hooks, scheduled work, route handlers — never loads during a page render at > all; put storefront code and back-office code in separate files and each page only pays > for what it uses. - `schedule`: (Optional) Cron expression to run a script on a schedule. A scheduled script can also be triggered **on demand** by the shop from the **⚡ Actions** menu on the plugin's settings, where it is listed under **Scheduled** with its cron expression, independently of its cadence — even for `private`-source plugins where the merchant can't see the code. Write scheduled scripts to be idempotent and safe to run off-schedule. Only declared scheduled scripts (a plain script with a `schedule`, or one inheriting the plugin-level schedule) are runnable this way; hook/route/widget scripts and internal `lib` files are not. - `plans`: (Optional) Ordered list of plan keys for tiered pricing, e.g. `["free", "pro", "biz"]`. Prices and features are set at publish time; the active key is exposed to scripts as `ctx.plan`. See [Plugin Plans](#plugin-plans-tiered-pricing). - `secrets`: (Optional, top-level) Declares the credentials the plugin expects so they appear as labeled entries in the Secrets panel. See [Declaring secrets in the manifest](#declaring-secrets-in-the-manifest). **Hook auto-detection.** Hook names are auto-detected from each script's `module.exports` — you don't list them. If a **registered** file (one named in `scripts`, or a widget's `source.script`) has a syntax error so it would register **no** hooks, install / update / activation **fails** with the offending file and error rather than silently activating with a partial set of hooks — fix the reported file and re-push. A syntax error in an *unregistered* `.js` file (a `require()`'d helper, a browser script served via a `` in a title or summary — would end the block early and spill the rest of the page as markup. The escape keeps the value valid JSON while making it inert as markup. #### Account section pages (`account_active`) A route under `/account/…` can render as a **section of the customer's account area** — inside the same sidebar navigation the built-in Order History / Subscriptions / Profile / Addresses pages use — instead of a standalone full-width page. Opt in by returning an `account_active` binding naming your section: ```javascript module.exports.fetch = function (ctx) { return { template: "./templates/account/wishlist.liquid", bindings: { account_active: "wishlist", items } }; }; ``` and wrapping your template's content in the account layout with the shared nav snippet: ```liquid
``` When `account_active` is present and the visitor is logged in, the platform injects the bindings the nav snippet needs (`has_subscriptions`, `has_addresses`) and marks the page noindex. Your sidebar link comes from `account_menu` (see "Contributing storefront menu entries" above) — the nav renders those entries after its built-in links and highlights the one whose `url` matches the current path. The bundled **Wishlist** and **Returns** plugins are worked examples. #### Returning a fragment instead of a page (`layout`) A `template:` response renders inside the theme's layout, because it is a page a visitor navigated to. Return **`layout: false`** and the template renders on its own — no header, no footer — which is what you want when the browser is fetching a piece of a page to drop into one it is already showing: ```javascript module.exports.fetch = function (ctx) { const product = sw.products.get(parseInt(ctx.request.query.id, 10)); if (!product) return { status: 404, body: { error: "not found" } }; return { template: "./snippets/card.liquid", layout: false, // a fragment, not a page cache: true, bindings: { product } }; }; ``` A fragment is rendered exactly the way a page is, which is the point: your template can `{% include %}` the theme's own snippets, and whatever other plugins contribute to what it renders (a badge on a product card, say) is in the HTML you hand back. Building the same HTML with `sw.liquid.render` does **not** do this — that renderer is for composing markup inside a hook and does not run other plugins' `hook.*` / `block.*` handlers. `layout` also accepts a path (`"./layouts/plain.liquid"`, resolved inside your plugin, or a theme layout by name) when you want a wrapper of your own. > **A fragment is a good fit for `cache` below**, and the cache key is the URL — > so ask for one thing per request. A route that takes a *list* of ids > (`?ids=4,9,12`) caches each combination separately and almost never sees the > same one twice; one that takes a single id (`?id=4`) is shared by every visitor > who asks for it. #### Edge-caching a route response (`cache`) By default a route response is **never** edge-cached — every request runs your script. For a page whose HTML is identical for every anonymous visitor (a blog post, a public listing, a marketing page), add **`cache: true`** to the response to serve it from the CDN edge with the same stale-while-revalidate policy regular storefront pages use. The platform handles the rest: ```javascript module.exports.fetch = function (ctx) { return { template: "./templates/article.liquid", cache: true, // edge-cache for anonymous visitors bindings: { article } }; }; ``` `cache` works on **buffered** responses — both `template`+`bindings` and `status`+`body`+`headers`. Pass `cache: { max_age: 300 }` to widen the freshness window (seconds); a bare `true` uses the platform default (~10s fresh, then served stale for up to 7 days while it revalidates in the background). The edge cache keys on the **full URL including the query string**, so paginated/filtered variants (`?cursor=…`, `?page=…`) cache as distinct entries. The backend enforces the safety rails — opting in **requests** caching, it never forces it: - **Logged-in customers always get a fresh, private response.** A request carrying a `customer_token` is never edge-cached (personalized pricing). Only anonymous visitors share the cached page. - **`GET` only**, and **never in dev mode or theme preview** (you always see fresh output while editing). - **Streamed responses (`write`) are never cached** regardless of `cache`. - **A failed run never replaces a good page.** If your route errors or returns a `5xx`, that response is not cached, and a visitor who would have hit the cache is served the last good copy of the page instead of the error. The moment your route succeeds again, the fresh response takes over. > **You own correctness-of-variance.** `cache` is safe only when the response > depends on nothing beyond the URL and the anonymous/logged-in split. If your page > varies by a custom cookie, geo (`X-Geo-*`), or any per-visitor signal, **don't set > `cache`** — the edge would serve one visitor's page to the next. Prefer setting > `cache` from a plugin **setting** so the store owner can disable it (the bundled > **Blog** plugin does this via its *Edge Cache Blog Pages* checkbox). #### Visitor network data (IP, geo, bot) `ctx.request` is available in **fetch route handlers and in every event hook** (`template.before_render`, `checkout.before_create`, `order.*`, …) — same shape everywhere: `{ method, url, path, proto, headers, query }`. The visitor's network signals ride in `ctx.request.headers` as values the platform resolves itself, never ones the visitor supplied: | Header | Meaning | | --- | --- | | `X-Real-Ip` | Visitor IP (always present; read from the connection itself, not from anything the client sent). | | `X-Geo-Country` | ISO country code, e.g. `US`. | | `X-Geo-Region` | Region/state name. | | `X-Geo-City` | City name. | | `X-Geo-Latlong` | `"lat,long"` (single header), e.g. `"37.77,-122.42"`. | | `X-Geo-Postal` | Postal/ZIP code. | | `X-Bot-Score` | Bot-detection score `1`–`99` (low ⇒ likely bot). | | `X-Verified-Bot` | `"1"` for a known-good crawler (Googlebot, etc.), else `"0"`. | | `User-Agent` | The raw UA string (passes through untouched). | ```javascript const h = ctx.request.headers; const ip = h["X-Real-Ip"]; const country = h["X-Geo-Country"]; const isBot = h["X-Verified-Bot"] === "1" || Number(h["X-Bot-Score"] || 100) < 30; ``` **Trust & availability.** - The platform **resolves** these itself on every request and strips any client-supplied `X-Geo-*` / `X-Bot-*` / `X-Real-Ip` copies before re-injecting its own — so a visitor **cannot forge them**. A value the platform could not resolve for itself is never passed on as if it had: you get the header with a value the platform stands behind, or you get no header. There is no third state in which a shopper picks their own country or address. - `X-Real-Ip` is always present. **Geo and bot headers are only present when the platform could determine them** — treat a missing header as "unknown" and fail open. `X-Bot-Score` / `X-Verified-Bot` additionally depend on bot detection being available for the store; without it they're absent. - **Varying a cached page by geo.** Storefront pages are stored and reused, so a `template.before_render` hook that changes what a page shows based on `X-Geo-*` will show one visitor's result to the next. Either keep the variation client-side, or have the merchant enable **Pages that change by country** (Settings → Developer), which makes the visitor's country part of the storage key. It splits by country only — a page varying on city, postal code or coordinates still needs the client-side approach. Marking the response uncacheable also works and is the right call for a page that is genuinely per-visitor. - `checkout.before_create` now carries `ctx.request`, so a checkout blocker can decide on IP/country/bot in addition to `ctx.data.order` email/phone. Throw `{ error, redirect_url }` to block (see the checkout hook section). **Note on Customer Context:** The execution context (`ctx`) provides information about the logged-in user differently depending on the hook type: - **`fetch` route handlers**: Receive `ctx.customer_id` (the ID of the logged-in customer, or `0` if guest). Because `fetch` handlers run with full access to the Bridge API, you can query the full customer record if needed via `sw.customers.get(ctx.customer_id)`. - **Render Hooks (`template.before_render`, `hook.*`) and storefront cart→checkout hooks (`cart.calculate_prices`, `coupon.validate`, `shipping.calculate`, `tax.calculate`, `checkout.before_create`)**: Receive the full customer object as `ctx.customer` (e.g., `ctx.customer.id`, `ctx.customer.email`, `ctx.customer.price_level`, `ctx.customer.payment_method`, `ctx.customer.payment_gateway`) when the shopper is logged in — `ctx.customer` is absent for guests (guard with `if (ctx.customer) { … }`). It's a read-only snapshot; secret fields are never exposed. This lets a pricing/coupon/shipping hook vary its result by the signed-in shopper (B2B tier, saved-method gateway, etc.) without a separate `sw.customers.get` lookup. ## Dashboard Widgets Plugins can ship widgets that render inside the ShopsWired admin panel — on the dashboard, as a left-menu page, or both. Widgets render server-side using the same `exports.fetch(ctx)` convention as route handlers, then are served into a sandboxed iframe with strict CSP and its own short-lived, scoped session. There is no client-side SDK to learn: write JavaScript + Liquid + `fetch`, exactly like a route plugin. ### Manifest A widget is declared in two places in `manifest.json`: 1. A `scripts[]` entry with `type: "widget"` mapping the script to a widget id. 2. A `widgets[]` array entry describing the widget shown in the dashboard gallery and (optionally) the left-menu. ```json { "scripts": [ { "path": "widgets/recent-reviews.js", "type": "widget", "widget_id": "recent-reviews" } ], "widgets": [ { "id": "recent-reviews", "name": "Recent Reviews", "icon": "⭐", "description": "Shows the most recent approved customer reviews.", "source": { "type": "internal", "script": "widgets/recent-reviews.js" }, "permissions": ["read:records:review"], "config_defs": [ { "key": "limit", "type": "number", "label": "Number of reviews", "default": 5 } ], "defaults": { "width": "1/2", "rows": 4, "config": { "limit": 5 } }, "placement": { "dashboard": true, "page": { "menu": true, "group": "Reviews" } } } ] } ``` Schema fields: - `id` / `name` / `icon` / `description` — gallery card display. - `source.type` — `"internal"` (this plugin renders the widget in its own server-side script), or `"link"` for an entry that only opens a URL (see [Linking to a page instead of opening a widget](#linking-to-a-page-instead-of-opening-a-widget-url)). - `source.script` — for internal widgets, the path that matches the `scripts[].path` declared above. - `permissions` — display-only string list shown in the gallery (e.g. `"Reads: products, orders"`). Enforcement happens via the existing per-plugin `sw.*` scoping; this field is documentation for the shop admin. - `config_defs` — per-instance config UI schema, same shape as `settings[]` (`text`, `number`, `checkbox`, `select`, `color`, `textarea`). - `defaults.width` — one of `"1/4"`, `"1/3"`, `"1/2"`, `"2/3"`, `"full"`. - `defaults.rows` — initial height in grid rows (1 row ≈ 80px). The dashboard grid snaps widgets to row boundaries so layouts stay aligned. Resize a widget to any number of rows from the config panel. - `defaults.config` — initial config values applied when the widget is added. - `placement.dashboard` — show in the dashboard gallery (default true). - `placement.page.menu` — also expose as a full-page entry in the left sidebar. Group adjacent page widgets under a header via `placement.page.group`. - `placement.detail` — render the widget on an entity **detail page**, bound to one record (see [Detail-page widgets](#detail-page-widgets-tabs--buttons) below). - `roles` / `placement.page.roles` — restrict a widget/page to specific built-in shop roles (role-name → level map). Note: custom-record access has moved to a permission-keyed `permissions` map (see [Gating custom records with permissions](#gating-custom-records-with-permissions)); widget/page `roles` remain role-name based for now. - `styles` — `""` (default) applies the bundled classless `sw-widget` stylesheet to the iframe body. `"none"` opts out so the widget controls its own styling (the html/body reset still ships). ### Detail-page widgets (tabs & buttons) A widget can also appear on an entity's **admin detail page** — the order, product, customer, coupon, fulfillment, or custom-record view — bound to that one record. The same `fetch(ctx)` script renders it; the only new thing it sees is **`ctx.widget.entity = { type, id }`** identifying the record. Use it to load the entity (`sw.orders.get(ctx.widget.entity.id)`, etc.) and render record-specific UI: a fulfillment panel on an order, a supplier tab on a product, a loyalty summary on a customer. Declare it with one or more `placement.detail` entries (a widget may have several — e.g. a tab and a button for the same entity): ```json "placement": { "detail": [ { "entity": "order", "mode": "tab", "label": "Shipments" }, { "entity": "order", "mode": "button", "label": "Refund", "variant": "danger", "size": "md" }, { "entity": "order", "mode": "menu", "label": "Resend invoice" } ] } ``` - `entity` — `"order"`, `"product"`, `"customer"`, `"coupon"`, `"fulfillment"`, `"subscription"`, or a custom record type as `"custom:"` (e.g. `"custom:rfq"`). (The viewer must have read access to that entity, or the widget is hidden.) - `mode` — `"tab"` (default) adds the widget as a **first-class tab** in the detail page's tab bar, sitting beside the native tabs (Details / Related / Memos) and deep-linkable via `?tab=app::`; `"button"` adds an action button to the page's standard button bar that opens the widget in a modal; `"menu"` adds a row to the page's **⋮ (more actions) menu** — same modal, lower emphasis, for actions that don't deserve a permanent button (it's also where a [link placement](#linking-to-a-page-instead-of-opening-a-widget-url) belongs); `"field"` (custom record types only) marks the widget as a **record-field editor** — it never appears as a tab, button or menu row, and is instead embedded by a `custom_records` field that names it via `"type": "widget"` (see [Record-field widgets](#record-field-widgets-mode-field)). - `label` — tab title / button or menu text (defaults to the widget `name`). - `icon` — optional leading glyph (tab, button or menu row). - `variant` *(button/menu)* — `"primary"`, `"secondary"` (default), or `"danger"`. - `size` *(button/menu)* — modal size `"sm"`, `"md"` (default), or `"lg"`. - `url` *(button/menu)* — makes the entry a **link** instead of a widget launch; see below. - `roles` — optional per-placement role gate (role-name → level), applied in addition to the widget-level `roles`. - `condition` — optional `" == "` expression (same syntax as a settings field's [`condition`](#conditional-fields-condition)). The placement only appears — and can only be launched — when the plugin's effective settings satisfy it, so it can be toggled by a merchant setting. Evaluated **server-side** against the merged settings (manifest defaults included), e.g. `"condition": "enable_radar == true"`. Empty/omitted = always shown. The bundled **`stripe-payment`** plugin uses this to show a **Risk** tab on the order page only when the merchant turns on its `enable_radar` setting. Tabs are **lazy** — a tab is only rendered the first time it's opened. A button or menu row costs nothing until clicked. #### Linking to a page instead of opening a widget (`url`) A `menu` (or `button`) placement that sets **`url`** doesn't run your widget at all: it renders as a link that opens the target in a **new tab**. This is how a record gets a "View on Storefront" action — the bundled **`blog`** plugin points its post records at their published page: ```json { "entity": "custom:blog", "mode": "menu", "label": "View on Storefront", "url": "{settings.path_prefix}/{record.slug}" } ``` The `url` may be a **path** (resolved against the store's storefront address, as above) or a full `https://…` address for an external tool. These tokens are filled in for you: | Token | Value | | --- | --- | | `{storefront_url}` | The store's storefront address, no trailing slash (only needed when the link isn't a plain path). | | `{settings.}` | One of your plugin's settings — so a merchant-configurable path like `path_prefix` stays correct. | | `{id}` | The id of the record on screen. | | `{record.}` | A field of the record on screen (`{record.slug}`, `{record.email}`, …). Values are URL-encoded, so a value with a slash or a space stays a single path segment. | | `{record.\|path}` | Same, for a field that already holds a **whole path** (`"/about-us"`): slashes are kept, each segment is still encoded, and a leading slash is added if the value lacks one. The bundled **`page`** plugin links a page record at `"{storefront_url}{record.path\|path}"`. | Only `http`/`https` targets are ever rendered, and a row is **hidden** rather than pointing somewhere wrong when a token has no value on this record (a post with no slug yet) or a `{settings.…}` key doesn't exist. A link placement needs no script — declare the widget with `"source": { "type": "link" }` and keep it out of the dashboard gallery: ```json "widgets": [ { "id": "view-post", "name": "View on Storefront", "source": { "type": "link" }, "placement": { "dashboard": false, "detail": [ { "entity": "custom:blog", "mode": "menu", "label": "View on Storefront", "url": "{settings.path_prefix}/{record.slug}" } ] } } ] ``` **Closing a button modal and refreshing the page.** A button or menu widget that finishes an action (e.g. a refund succeeded) calls **`sw.close()`** to dismiss its modal. By default the host then **re-fetches the underlying record** so the detail page reflects the change; pass `sw.close({ refresh: false })` to skip the reload. `sw.close()` is a no-op for tab/page/dashboard widgets. A tab widget that mutated the record can also call `sw.close({ refresh: true })` to ask the page to reload (there's no modal to dismiss). Client scripts that don't render on the server can read the bound record from **`sw.entity`** (`{ type, id }`, or `null` outside a detail page) — the client-side mirror of `ctx.widget.entity`. ### Record-field widgets (mode: "field") A `custom_records` field declared as `{"name": "items", "type": "widget", "widget": "", "label": "Items"}` is rendered on the record **edit page** by your widget instead of the built-in JSON editor — a purpose-built view/editor for structured data (line items, schedules, matrices). Wire it up in three parts: 1. The field names one of your own widgets via `widget`. 2. That widget declares the placement `{"entity": "custom:", "mode": "field"}` — this authorizes it to bind to records of that type; field-mode placements never appear as tabs or buttons. 3. The widget's `fetch(ctx)` reads the record itself (`sw.records..get(ctx.widget.entity.id)`) and renders the field's current value. **Editing** goes through **`sw.field.set(value)`** (client-side, from the widget's markup/JS): it pushes the edited value into the record form's unsaved state — the merchant still clicks the form's own **Save** to persist, exactly like every other field. Call it on each change (or on a "Done" action); calling it repeatedly just replaces the pending value. It is a no-op outside a field placement. The value is stored like a `json` field: any structure, not filterable. **The widget knows what it's editing.** A field render receives, on top of the usual widget context: `ctx.widget.placement === "field"`, `ctx.widget.entity` (`{type: "custom:", id}`), **`ctx.widget.field`** (the field name from the record's schema) and `ctx.widget.id` (the widget id being rendered). The same trio is mirrored client-side as `sw.entity`, **`sw.field.name`** and the widget id you already know. So one script can be reused broadly: - **One widget id, many fields/record types** — branch on `ctx.widget.field` / `ctx.widget.entity.type`. Declare one `mode: "field"` placement per record type it serves; each field that names the widget binds it (the platform verifies a launch's field is actually declared with `"type": "widget", "widget": ""` on that record type, so a widget can't be attached to arbitrary fields). - **One script file, many widget ids** — declare several `widgets[]` entries whose `source.script` (and matching `scripts[]` entries) point at the same file, then branch on `ctx.widget.id`. Prefer this when two fields of the **same record** use the same editor, so each iframe keeps its own session. Notes: - On a **new** (unsaved) record there is no id to bind, so the form shows a placeholder until the record is first saved; design the field to tolerate an empty value. - Read-only viewers see the widget too — the form simply has no Save for them, so pending `sw.field.set` values go nowhere. You can also branch on `ctx.widget.user.permissions` to hide edit controls. ### The `fetch(ctx)` signature Widget scripts export `fetch(ctx)`. The same function handles both the initial GET that renders the iframe and any in-iframe AJAX — branch on `ctx.request.method` and `ctx.request.path` exactly as in a route plugin. Most widgets only need the GET render because the dashboard hover toolbar provides a refresh action that reloads the iframe; reach for in-iframe AJAX (`window.sw.fetch`) only for user-initiated interactions inside the widget body (filters, row-level actions, etc.). ```javascript // widgets/recent-reviews.js module.exports.fetch = function (ctx) { const limit = (ctx.widget.config && ctx.widget.config.limit) || 5; const reviews = sw.records.review.list({ filters: { status: 'Approved' }, limit }); return sw.liquid.render('./widgets/recent-reviews.liquid', { reviews: (reviews && reviews.items) || [], ctx }); }; ``` `ctx` extends the normal route handler context with a `widget` field carrying per-instance state: - `ctx.widget.id` — the widget schema id (e.g. `"recent-reviews"`). - `ctx.widget.key` — the stable per-widget identifier within the containing dashboard. Empty when the widget is rendered as a page. Use this for `sw.storage` keys when you want isolated state per widget instance. - `ctx.widget.dashboard_id` — id of the containing dashboard (only set for dashboard widgets; omitted for page widgets). - `ctx.widget.page` — `true` when rendering as a left-menu page (no dashboard membership). Per-instance config falls back to `defaults.config` since there's no dashboard to read from. - `ctx.widget.entity` — for [detail-page widgets](#detail-page-widgets-tabs--buttons), the record the widget is bound to: `{ type, id }` where `type` is `"order"`, `"product"`, `"customer"`, `"coupon"`, `"fulfillment"`, `"subscription"`, or `"custom:"`. Omitted for dashboard/page widgets. Server-trusted (it's provided by the platform, server-side), so load the record straight from it. - `ctx.widget.placement` — where the widget is rendering: `"dashboard"`, `"page"`, `"tab"`, `"button"`, or `"menu"`. Lets one widget adapt its UI to context — e.g. show a Close button (`sw.close()`) only when it's in a modal (`"button"` / `"menu"`), or hide it in a `"tab"`. Provided by the platform, so it's server-trusted. - `ctx.widget.config` — the merged config (defaults + per-instance overrides set via the config panel). - `ctx.widget.user` — `{ id, email, role, permissions }` of the admin currently viewing the widget. - `ctx.widget.shop` — `{ id, name, currency, canonical_url, canonical_host }`. `canonical_url` is the shop's public storefront origin (custom domain if set, otherwise `.shopswired.com`), no trailing slash — use it to display/build absolute storefront links from an admin widget (the widget renders on an admin origin, so the request host is not the storefront). - `ctx.role` — the viewer's built-in shop role (`owner` / `admin` / `staff`). Also under `ctx.widget.user.role`. - `ctx.permissions` — array of this plugin's [declared permissions](#permissions) the viewer holds (de-namespaced). Also under `ctx.widget.user.permissions`. Gate behavior on these server-side, e.g. `ctx.permissions.includes('view_orders')`. - `ctx.widget.csrf` — random CSRF token bound to the session cookie; inject into your Liquid templates for any POST endpoint your widget exposes. - `ctx.widget.base` — string path-prefix shared by every URL under this widget instance (e.g. `/w/1/reviews/recent-reviews/42`). The iframe's CSP only allows `connect-src 'self'`, so any in-iframe AJAX must hit this prefix — the bundled `window.sw.fetch` helper does this for you (see [Built-in styles and helpers](#built-in-styles-and-helpers) below). - `ctx.widget.url(path)` — same as `base` but as a function for scripts that prefer a call: `sw.fetch(ctx.widget.url('refresh'))`. Equivalent to `ctx.widget.base + '/' + path`. Not callable from Liquid (Liquid has no method-call syntax). All other `sw.*` bridges (`records`, `products`, `orders`, `customers`, `storage`, `liquid`, `assets`, etc.) work as in any other plugin script. **Linking your own static assets — always use `sw.assets.url()`, never a hardcoded `/plugin-assets/...` path.** A widget renders on its own origin (`w-.shopswired.com` in prod), which carries no shop in the hostname, so a literal `/plugin-assets//file.js` cannot resolve the shop and 404s. `sw.assets.url('assets/grapes.min.js')` returns a shop-scoped, same-origin URL that resolves correctly on the widget origin (and on the storefront, where the same call is also the right way to reference an asset). Loaded same-origin, it satisfies the widget CSP's `script-src 'self'` / `style-src 'self'` without any cross-origin allowance — so self-hosted CSS/JS/fonts just work. Resolve the URL server-side in `fetch(ctx)` and seed it into your markup (e.g. a `window.MY_ASSETS` object or a Liquid binding); don't reconstruct the path in client JS. The URL it returns carries a `?v=` cache-busting tag taken from your manifest `version`, so **bumping your plugin version is what makes shoppers fetch changed JS/CSS** instead of a browser-cached copy. A `/plugin-assets//…` path piped through Liquid's `cdn_url` filter (in a snippet you render yourself) gets the same plugin-version tag. ### Return values The dispatcher accepts three return shapes from `fetch(ctx)`: - **String** — treated as HTML. On the initial GET render, the dispatcher wraps the string in the iframe shell (CSS reset, postMessage shim, CSP headers). On subsequent requests, the string is returned as-is. - **`{ html, status?, headers? }`** — explicit HTML response with no wrapper. Use this when your widget controls the full document. - **`{ json, status?, headers? }`** — JSON response. Use for AJAX endpoints called from inside the iframe. ### Liquid templates Templates resolve relative to the plugin root with `./` prefix. The iframe shell wraps your output in a body that already has `class="sw-widget"`, so the bundled stylesheet (see below) styles everything you write to match the admin theme — just emit semantic HTML, no wrapper or inline `