# NanoCart > NanoCart (https://nanocart.io) is an e-commerce platform for small stores: an > embeddable cart + checkout widget for any website, and hosted storefronts at > {store}.nanocart.io. Flat monthly pricing, 0% NanoCart transaction fees. Payments run > through the merchant's own Stripe and/or PayPal accounts. Key facts for AI agents: - Widget embed: `` (the attribute is data-store-id; the Store ID is public). - Buttons: data-nanocart-buy / data-nanocart-add (add first variant + open cart drawer), data-nanocart-product (detail modal WITH variant selector). - Theming: CSS variables --nc-* on the #nanocart-widget host. - Admin API: base https://api.nanocart.io, header `x-api-key: sc_live_...` (SECRET — server-side only). All prices in integer cents. - Official AI coding-agent skill: https://cdn.nanocart.io/ai/nanocart-skill.zip - Official MCP server (live store tools): npx nanocart-mcp, or hosted at https://mcp.nanocart.io/mcp?store=STORE_ID with Authorization: Bearer - Dashboard: https://portal.nanocart.io · API reference: https://nanocart.io/docs --- # ===== Nanocart Overview ===== (https://docs.nanocart.io/#getting-started-intro) # nanocart — Support Knowledge Base Overview ## What Is nanocart? nanocart is a lightweight SaaS e-commerce platform designed for small businesses, indie sellers, and developers who want to add a fully functional shopping cart to an existing website without rebuilding their site or switching platforms. It works as a thin cart layer that sits on top of any existing site or as a fully hosted storefront — your store, your Stripe account, your customers. **Key links:** - Admin panel: https://nanocart.io/admin - API: https://api.nanocart.io - Demo store: https://nanocart.io/demo (Cheeky Surf Shop) - Support: hello@nanocart.io --- ## Two Ways to Use nanocart ### 1. Widget Embed (Add a Cart to Any Existing Site) The widget embed is nanocart's core use case. Drop a single ` ``` **Important:** `data-store-id` must exactly match the storeId in your admin panel — it is lowercase, alphanumeric, and may contain hyphens (e.g., `cheeky-surf-shop`). A mismatch will prevent the widget from loading your products. Place before `` to avoid blocking page render. Placing in `` also works but adds a small render-blocking penalty. --- ## Button Data Attributes Three `data-` attributes turn any HTML element — ` ``` ### `data-nanocart-add` — Add to Cart Functionally identical to `data-nanocart-buy` (adds to cart and opens the drawer), but semantically signals "add to cart" rather than "buy now." Use whichever label fits your UI. ```html Add to Cart ``` ### `data-nanocart-product` — View Product Details Opens a product detail modal with the product image, description, variant selectors, and an add-to-cart button. Does not add anything to the cart automatically. ```html
View Details
``` **Complete example using all three:** ```html My Shop

Classic Surfboard

See Details ``` --- ## JavaScript API (`window.nanocart`) After the widget script loads, a `window.nanocart` object is available with the following methods. For reliable access, listen for the `nanocart:ready` event before calling any methods. ```html ``` | Method | Description | |---|---| | `nanocart.addBySlug('slug')` | Programmatically add a product to the cart by slug | | `nanocart.viewProduct('slug')` | Open the product detail modal for a given slug | | `nanocart.open()` | Open the cart drawer | | `nanocart.close()` | Close the cart drawer | | `nanocart.toggle()` | Toggle the cart drawer open/closed | | `nanocart.getItems()` | Returns an array of current cart items | | `nanocart.getCount()` | Returns the total item count (integer) | | `nanocart.getSubtotal()` | Returns the cart subtotal **in cents** (e.g., `4999` = $49.99) | | `nanocart.clear()` | Empties the cart | **Example — custom cart icon with item badge:** ```html ``` --- ## `` Web Component The `` custom element renders a self-contained product card — image, name, price, variant selectors, and an add-to-cart button — anywhere on the page. Place the tag wherever you want the card to appear. ```html ``` **Key characteristics:** - **Shadow DOM isolated:** The component's internal styles do not bleed into your page CSS, and your page CSS cannot accidentally override the card's styles. This makes it safe to drop onto any existing design. - **Multi-axis variants:** When a product has multiple option axes (e.g., Size AND Color), each axis gets its own dropdown selector. The widget resolves the correct variant using an `optionValues` map on each variant object. - **Multiple instances:** Place as many `` elements on a single page as needed — each is independent. **Full page example:** ```html Product Showcase

Featured Products

``` --- ## Cart Behavior and Checkout - **Persistence:** Cart state is stored in `localStorage` and survives page reloads, browser back/forward navigation, and tab restores. - **Checkout:** Clicking the checkout button in the cart drawer redirects the customer to a Stripe-hosted Checkout page. The widget uses the page's `Referer`/`Origin` header to set the return URL, so after payment the customer lands back on your site. - **Order confirmation:** The widget stores the `orderId` in localStorage before the Stripe redirect. On the return page the widget detects this value and can display an order confirmation message. - **Prices:** All price values returned by the API and `getSubtotal()` are in **cents** (integer). $49.99 is represented as `4999`. --- ## CORS and `allowedDomains` If your store has the `allowedDomains` setting configured in the admin panel, the nanocart public API will only accept requests from those domains. Because the widget runs on your website (your domain), **your domain must be listed in `allowedDomains`**. - Leave `allowedDomains` empty to allow requests from any origin (suitable during development or for stores with no restriction requirements). - Add each domain exactly as it appears in the browser (e.g., `https://www.example.com`), including protocol and without a trailing slash. --- ## Troubleshooting Common Issues ### Widget not loading - Open browser DevTools → Network tab. Look for a failed request to `cdn.nanocart.io`. A 404 suggests the script URL is wrong; a network error suggests an ad blocker or firewall is interfering. - Check the browser Console for errors referencing `nanocart`. - Verify `data-store-id` exactly matches the storeId in your admin panel — it is case-sensitive and must be all lowercase. ### Button not responding to clicks - Confirm the slug in the `data-nanocart-*` attribute exactly matches the product slug shown in the admin panel. - Verify the product status is **active** in the admin panel. Inactive or draft products will not load. - Confirm the ` ``` ### CORS errors in the console - Add your website's domain to the `allowedDomains` list in the nanocart admin panel, or clear the list to allow all origins. - Check that the domain is listed exactly as the browser sends it (including `https://` and no trailing slash). ## Theming the Widget The widget is fully themeable so the cart matches your website. Three official ways, no JavaScript ever required: **1. Portal (no code):** portal.nanocart.io → **Settings > Widget Appearance** — base theme (dark/light), accent/background/surface/text colors, corner radius. Live widgets update within ~5 minutes. Accent falls back to your Brand Color when a base theme is set without one. **2. Script-tag attributes** (always beat portal settings): ```html ``` **3. CSS variables (full control):** the widget's shadow host has the stable id `nanocart-widget`; declare `--nc-*` variables on it from your own stylesheet — this beats every other layer: ```css #nanocart-widget { --nc-accent: #EF3E32; --nc-bg: #1d2230; --nc-radius: 12px; } ``` Stable variable API: `--nc-accent`, `--nc-bg`, `--nc-surface`, `--nc-border`, `--nc-text`, `--nc-text-muted`, `--nc-text-dim`, `--nc-text-strong`, `--nc-danger`, `--nc-success`, `--nc-radius`, `--nc-font` (dark defaults: #4292e7 / #1a1f2e / #242938 / #2d3348 / #e2e8f0 / #94a3b8 / #64748b / #ffffff / #ef4444 / #22c55e / 8px / system fonts; light theme swaps the neutral palette). **Precedence** (highest wins): your page CSS → script-tag attributes → portal Widget Appearance → built-in defaults. # ===== Products ===== (https://docs.nanocart.io/#products-managing) # Products — nanocart Support Knowledge Base ## What Is a Product? A product in nanocart is the core purchasable item in your store. Products appear in your storefront and widget, can be organized into categories, and support variants (e.g., different sizes or colors). Every product belongs to a specific store and is identified by a unique slug used in widget button attributes. Products can be physical (require a shipping address at checkout) or digital (no shipping collected; download link delivered via order confirmation email). A product without variants is a single purchasable item; a product with variants offers multiple purchasable versions of the same item. --- ## Product Fields ### Required Fields - **name** — The display name shown in the storefront, product modals, and cart. Required on creation. - **price** — Base price in integer cents (e.g., `1999` = $19.99). Required. When variants exist, each variant can override this price; the base price acts as a fallback. ### Identity and Discovery - **slug** — A URL-safe identifier (lowercase letters and hyphens). Auto-generated from the product name if not set manually (e.g., "Blue T-Shirt" → `blue-t-shirt`). Must be unique within the store. Used in widget `data-nanocart-buy`, `data-nanocart-add`, and `data-nanocart-product` attributes. Can be set manually at creation; changing a slug after launch will break existing widget buttons. - **status** — `"active"` (visible and purchasable) or `"draft"` (hidden from public, cannot be purchased). Newly created products default to `"draft"`. Only active products count toward tier limits. - **featured** — Boolean. When `true`, storefronts can display the product in hero or featured sections. Set to `false` by default. - **tags** — Array of strings for internal organization and filtering. Not shown to customers. ### Pricing and Sale Display - **compareAtPrice** — Strikethrough/original price in integer cents. Must be greater than `price`. When set, the widget displays a "sale" badge alongside the discounted price. Leave unset if no sale is running. ### Description and Media - **description** — Product description. HTML is allowed and rendered in product modals and storefront pages. - **images** — Array of image URLs. The first image in the array is used as the primary/thumbnail image. Upload images via the presigned URL flow described below. ### Inventory and Shipping - **inventory** — Integer stock count. Set to `null` for unlimited stock. Decremented automatically when a purchase is completed via Stripe webhook. - **shippingCost** — Per-item shipping cost in cents. Used when the store's shipping method is set to `per_item`. ### Classification and Tax - **categoryId** — Assigns the product to a category. Categories must be created separately before they can be referenced here. - **productType** — `"physical"` (default) or `"digital"`. Digital products skip shipping address collection at checkout and deliver a download link in the order confirmation email. - **taxable** — Boolean, defaults to `true`. Set to `false` for tax-exempt items such as gift cards or non-taxable services. --- ## Variants and Options ### When to Use Variants Use variants when a single product comes in multiple purchasable versions — different sizes, colors, materials, or any other attribute. A product without variants has exactly one purchasable form. ### Options Options define the selection axes for your variants. Each option has a name and a list of values: ```json [ { "name": "Size", "values": ["S", "M", "L"] }, { "name": "Color", "values": ["Red", "Blue"] } ] ``` This example creates two axes. The widget renders separate dropdowns for each axis (one for Size, one for Color), allowing customers to make independent selections. ### Variant Objects Each variant represents one combination of option values. Variant fields: - **variantId** — Unique identifier for the variant. - **name** — Display name for the variant (e.g., "M / Red"). - **price** — Price in cents. Overrides the base product price for this variant. - **sku** — Optional SKU for inventory tracking. - **inventory** — Stock count for this specific variant (`null` = unlimited). - **optionValues** — Key-value map linking the variant to its option axis values, e.g., `{"Size": "M", "Color": "Red"}`. This is the preferred format and powers multi-axis selection in the widget and web component. The legacy flat name format (`"M / Red"`) is still supported for backward compatibility, but `optionValues` is preferred for new integrations. When variants are present, the base product `price` is a fallback only. Always set a `price` on each variant. --- ## Tier Limits Active product limits by plan: | Plan | Active Product Limit | |---|---| | Free | 2 | | Standard ($5/mo) | 25 | | Pro ($10/mo) | 100 | | Expert ($25/mo) | Unlimited | **Important:** Only `active` products count toward the limit. Products with `status: "draft"` or `status: "archived"` do not count. If a store hits its limit, any attempt to activate a product returns a `TIER_PRODUCT_LIMIT` error. --- ## Image Upload Flow nanocart uses presigned S3 URLs for image uploads. The process: 1. **Request a presigned URL** — `POST /shop/{storeId}/admin/upload-url` with body `{ "uploadType": "product_image" }`. The response includes a presigned `uploadUrl` and a `fileUrl`. 2. **Upload the file** — `PUT` the image binary to the `uploadUrl` with the correct `Content-Type` header (e.g., `image/jpeg`, `image/png`). 3. **Add to the product** — Use the `fileUrl` from step 1 in the product's `images` array when creating or updating the product. **File size limits by plan:** | Plan | Max Size | |---|---| | Free | 5 MB | | Standard | 25 MB | | Pro | 50 MB | | Expert | 500 MB | --- ## CRUD Operations | Action | Method | Endpoint | |---|---|---| | Create product | `POST` | `/shop/{storeId}/admin/products` | | Update product | `PUT` | `/shop/{storeId}/admin/products/{productId}` | | Archive product | `DELETE` | `/shop/{storeId}/admin/products/{productId}` | | List products | `GET` | `/shop/{storeId}/products` | | Get single product | `GET` | `/shop/{storeId}/products/{slug}` | - **Create**: All required fields (`name`, `price`) must be provided. Optional fields default as described above. - **Update**: Only send changed fields. `productId` must be included or provided in the URL path. - **Delete/Archive**: Sets `status` to `"archived"`. Products are never permanently deleted. Archived products are hidden from public and do not count toward tier limits. - **List query params**: `category` (filter by categoryId), `sort` (`newest`, `price`, or `featured`), `limit` (max 50), `lastKey` (for cursor-based pagination). --- ## Common Support Questions **Cannot activate a product — getting TIER_PRODUCT_LIMIT error** The store has reached its active product limit for the current plan. To resolve: archive unused active products (sets status to `archived`, frees up a slot), or upgrade to a higher tier. Draft and archived products do not count toward the limit. **Product not showing in the widget or storefront** The product must have `status: "active"`. Confirm the correct `storeId` is being used in the widget embed code. Draft and archived products are never returned to the public API. **Variant selector not appearing** At least 2 variants must be defined, or an `options` array with defined values must be present. A single variant with one option value will not render a selector. Add additional variants or option values to trigger the selector UI. **Images not loading** Use the exact `fileUrl` returned by the `/admin/upload-url` endpoint. Do not construct image URLs manually — the presigned URL and the public file URL are different. Ensure the `PUT` upload completed successfully before saving the URL to the product. **How to show a sale price / strikethrough price** Set `compareAtPrice` to an integer (cents) greater than `price`. The widget will automatically display a "sale" badge and show both the sale price and the original strikethrough price. **How to mark a product as featured** Set `featured: true` on the product via a `PUT` update. The storefront template must support featured display; not all templates surface this field automatically. **Digital product download link not received** Ensure `productType` is set to `"digital"`. Include the download link in the product `description` field so it appears in the product modal, and/or in the order confirmation email template. Digital products do not trigger shipping flows. **Slug conflict error on creation or update** Slugs must be unique within the store. If a slug is already in use (including by an archived product), choose a different slug or append a differentiator (e.g., `blue-t-shirt-v2`). **Inventory not decrementing after a purchase** Inventory is decremented via the Stripe webhook. Confirm the platform webhook is active and pointing to `https://api.nanocart.io/shop/admin/platform-webhook`. If inventory is `null`, it is set to unlimited and will never decrement. # ===== Categories ===== (https://docs.nanocart.io/#products-managing) # Categories ## What Are Categories? Categories organize your products into groups for navigation and filtering. Shoppers can browse products by category, making it easier to find what they're looking for. Each category has a unique slug within your store, which appears in URLs and API queries. Categories support a parent/child hierarchy, so you can create nested groupings (e.g., "Clothing" as a parent with "Shirts" and "Pants" as subcategories). There is no tier limit on the number of categories — create as many as your store needs. Each product is assigned to a single categoryId, so a product belongs to exactly one category at a time. --- ## Category Fields | Field | Description | |---|---| | `categoryId` | Unique UUID, auto-generated when the category is created | | `name` | Display name shown to shoppers | | `slug` | URL-friendly identifier, auto-generated from the name, unique within the store | | `description` | Optional text description for the category | | `image` | Optional image URL for category cards (upload via the `upload-url` endpoint using type `category_image`) | | `parentId` | Empty string `""` = top-level category; set to a parent's `categoryId` = subcategory | | `sortOrder` | Numeric display order — lower numbers appear first; default is `0` | | `status` | `"active"` = visible in public listings; `"hidden"` = not shown | --- ## Category Hierarchy nanocart supports a two-level hierarchy: **top-level categories** and **subcategories**. Going deeper than two levels (parent → child → grandchild) is not recommended and may not render correctly in storefronts. - **Top-level categories**: `parentId` is an empty string `""` - **Subcategories**: `parentId` is set to the parent category's `categoryId` (UUID) ### Important hierarchy behaviors - **Hiding a parent does not hide its children.** If you hide a parent category, its subcategories remain active and visible. You must hide each subcategory individually. - **Products belong to one category.** A product assigned to a subcategory does not automatically appear when browsing the parent category. To show all products under a parent (including those in subcategories), query each subcategory's products separately and combine them on the client side. --- ## CRUD Operations ### Create a category ``` POST /shop/{storeId}/admin/categories ``` Required: `name` Optional: `parentId`, `description`, `image`, `sortOrder`, `status` ### Update a category ``` PUT /shop/{storeId}/admin/categories ``` Required: `categoryId` Only include the fields you want to change — omitted fields are left as-is. ### Delete (hide) a category ``` DELETE /shop/{storeId}/admin/categories ``` This sets the category's status to `"hidden"`. Categories are **not permanently deleted** — they remain in the system but are excluded from public listings. ### List all active categories ``` GET /shop/{storeId}/categories ``` Returns all categories with `status: "active"`, sorted by `sortOrder`. --- ## Changing a Category's Position in the Hierarchy **Move a category to become a subcategory:** Send a PUT request with `parentId` set to the target parent's `categoryId`. **Promote a subcategory to top-level:** Send a PUT request with `parentId` set to an empty string `""`. --- ## Sorting Categories The `sortOrder` field controls display order — lower numbers appear first. When two categories share the same `sortOrder` value, they are sorted alphabetically by name. To reorder categories, send PUT requests updating each category's `sortOrder` value. --- ## Uploading a Category Image To set a category image: 1. Call the `upload-url` endpoint with type `category_image` to get a pre-signed upload URL. 2. Upload your image to that URL. 3. Use the returned image URL in the `image` field when creating or updating the category. --- ## Common Questions **"I hid a category but its products still show up in All Products."** This is expected behavior. Hiding a category removes it from category navigation and filtered browsing — it does not hide or deactivate the products assigned to it. Products remain visible in the general `/products` listing regardless of their category's status. To hide products, update each product's status individually. **"How do I show all products under a parent category, including products in its subcategories?"** Products are assigned to one categoryId. A product in a subcategory is not automatically returned when querying the parent. To aggregate: get the list of subcategories (filter where `parentId === parent.categoryId`), then query products for each subcategoryId, and combine the results on the client side. **"Is there a limit to how many categories I can create?"** No. There is no tier-based limit on categories. All plans — Free, Standard, Pro, and Expert — can create unlimited categories. **"My subcategory isn't showing up."** Check that `parentId` is set to the parent's `categoryId` (a UUID like `a1b2c3d4-...`), not the parent's name or slug. Using a name or slug instead of the UUID is the most common cause of subcategories not linking correctly to their parent. **"Can a product belong to multiple categories?"** No. Each product has a single `categoryId`. If you need a product to appear in multiple places, consider duplicating it or handling the grouping at the storefront display layer. **"What happens to products when I delete a category?"** The category is hidden (not permanently deleted), and the products previously assigned to it retain their `categoryId` reference. Those products will still appear in the general products listing. You may want to reassign them to another category via PUT requests on each product. # ===== Image Uploads ===== (https://docs.nanocart.io/#products-managing) # Image Uploads and File Storage nanocart stores all uploaded files in AWS S3 and serves them globally via CloudFront CDN. The nanocart server is **not** in the upload path — files go directly from the client to S3. This keeps uploads fast and removes any server-side bottleneck. ## Upload Flow (Two Steps) Every upload follows the same two-step process. ### Step 1 — Request a Presigned URL Send a `POST` request to nanocart to get a short-lived S3 upload URL: ``` POST /shop/{storeId}/admin/upload-url x-api-key: YOUR_API_KEY ``` Request body: | Field | Type | Description | |---|---|---| | `fileName` | string | Original filename (e.g. `hero-banner.jpg`) | | `contentType` | string | MIME type (e.g. `image/jpeg`) | | `uploadType` | string | One of: `product_image`, `digital_file`, `category_image`, `storefront_image` | | `fileSizeMB` | number | File size in decimal MB (e.g. `2.4`) | nanocart validates the file size and type here. If validation passes, the response contains: | Field | Description | |---|---| | `uploadUrl` | The presigned S3 URL — use this to upload the file | | `fileUrl` | The permanent CloudFront URL — use this to reference the file | | `s3Key` | The S3 object key | ### Step 2 — Upload the File to S3 Send an HTTP `PUT` request directly to the `uploadUrl`: - **Body**: raw file bytes - **Header**: `Content-Type` must **exactly match** what you specified in step 1 - **No auth headers needed** — the presigned URL handles authentication **Important:** Presigned URLs expire after 15 minutes. Request one and upload immediately. ### After the Upload Use the `fileUrl` (not the `uploadUrl`) wherever you need to reference the file — in a product's images array, the storefront logo field, a category image, etc. The `uploadUrl` is temporary and only works for uploading. The `fileUrl` is permanent. ## Upload Types and Storage Paths | `uploadType` | S3 path | Where to use the `fileUrl` | |---|---|---| | `product_image` | `{storeId}/products/{filename}` | Product `images` array via `PUT /shop/{storeId}/admin/products` | | `digital_file` | `{storeId}/digital/{filename}` | Download link for digital products | | `category_image` | `{storeId}/categories/{filename}` | Category `image` field | | `storefront_image` | `{storeId}/storefront/{filename}` | `logo`, `heroImage`, `aboutImage`, or `favicon` in storefront settings | ## File Size Limits by Tier | Tier | Max file size | |---|---| | Free | 5 MB | | Standard | 25 MB | | Pro | 50 MB | | Expert | 500 MB | Exceeding your tier's limit returns a `403 TIER_UPLOAD_LIMIT` error in step 1 — before any upload attempt is made. ## Supported File Formats - **Product images**: `image/jpeg`, `image/png`, `image/webp`, `image/gif` - **Category images**: `image/jpeg`, `image/png`, `image/webp` - **Storefront images (logo, hero, about)**: `image/jpeg`, `image/png`, `image/webp`, `image/svg+xml` - **Favicon**: `image/png`, `image/x-icon`, `image/vnd.microsoft.icon` - **Digital files**: any MIME type — PDF, ZIP, MP3, MP4, EPUB, etc. ## Recommended Image Dimensions | Image | Recommended size | Notes | |---|---|---| | Product image | 800×800 or 1200×1200 px | Square | | Category image | 400×300 px | Landscape | | Logo | 240×72 px | Landscape; transparent PNG strongly recommended | | Hero image | 1440×720 px | Landscape | | About page image | 800×600 px | | | Favicon | 32×32 or 64×64 px | Square | ## Using Images in Products Add the `fileUrl` to a product's `images` array. The **first URL** in the array is the primary image shown in the widget and storefront. ``` PUT /shop/{storeId}/admin/products { "productId": "...", "images": [ "https://...fileUrl1", "https://...fileUrl2" ] } ``` You can also use any publicly accessible external image URL in the `images` array or `logo` field — nanocart-hosted storage is not required. Just make sure the external URL stays publicly accessible. ## Digital Files Upload with `uploadType: "digital_file"`. The returned `fileUrl` is the download link. Include it in the product description or configure it in your order confirmation settings so customers receive it after purchase. ## CloudFront CDN Delivery All files are served via CloudFront for fast global delivery. The first request from a new CloudFront edge location may be slightly slower (cache miss). Subsequent requests from that region are cached and served quickly. ## Common Issues **"Presigned URL expired"** Presigned URLs are valid for 15 minutes. Request a fresh one from step 1 and upload immediately. **"CORS error when uploading to S3"** The `Content-Type` header in your `PUT` request must exactly match the `contentType` you specified in the `/upload-url` request. Any mismatch causes S3 to reject the upload. **"Image not showing after upload"** Use the exact `fileUrl` from the step 1 response. Do not manually construct S3 or CloudFront URLs. Confirm the `fileUrl` is saved to the correct field — product `images` array, `logo` field, etc. **"403 TIER_UPLOAD_LIMIT"** The file exceeds your tier's size limit. Compress the image (e.g. with TinyPNG) or upgrade your plan at [nanocart.io/admin](https://nanocart.io/admin). **"Image loads slowly on first visit"** This is a CloudFront cold-start on the first request from a new region. Subsequent visits are cached and fast. **"Can I use an external image URL instead of uploading?"** Yes. Any publicly accessible image URL works in the `images` array or `logo` field. Just ensure the URL remains publicly accessible long-term. # ===== Hosted Storefront ===== (https://docs.nanocart.io/#storefront-overview) # Hosted Storefront ## What Is the Hosted Storefront? The nanocart hosted storefront is a full-featured single-page application (SPA) hosted entirely by nanocart. When enabled, your store is live at `https://{storeId}.nanocart.io` (or your own custom domain on qualifying plans) — no servers, no code, no deployment needed. The storefront automatically pulls your products, categories, images, pricing, and store settings from your nanocart account. Everything is configured from the Admin Panel at https://nanocart.io/admin. There is no template editing or technical setup required. **This is completely separate from the nanocart widget.** Understanding the difference is important: | | Widget | Hosted Storefront | |---|---|---| | What it is | A JavaScript snippet you embed into your existing site | A complete store website hosted by nanocart | | Who hosts it | You (your existing website) | nanocart | | Best for | Stores that already have a website and want to add a cart | Stores that want nanocart to be their entire online store | | URL | Your existing site's URL | `{storeId}.nanocart.io` (or custom domain) | | Requires coding | Minimal (paste a script tag) | None | You can use both simultaneously. For example, embed the widget on your marketing homepage to let visitors buy directly there, and also maintain a hosted storefront at `yourstore.nanocart.io` as a standalone shopping destination. --- ## Your Store URL Every store gets a permanent URL automatically: ``` https://{storeId}.nanocart.io ``` For example, a store with ID `cheeky-surf-shop` is available at `https://cheeky-surf-shop.nanocart.io` the moment the hosted storefront is enabled. The storeId is assigned at store creation and **cannot be changed**. If you need a different URL, a custom domain (Growth plan and above) is the solution. --- ## Storefront Pages The following pages are generated automatically — no configuration required: - **Home** — Hero section, featured products, and category navigation - **Shop / All Products** — Full product grid with category filtering and sort options - **Product Detail** — Full product page with images, description, variant selector, and add-to-cart - **About** — Custom content page you can edit from the admin panel - **Cart** — Slide-out drawer (shared with the widget if you use both) - **Checkout** — Redirects to Stripe-hosted checkout for secure payment --- ## Hosted Plans The hosted storefront is a **separate subscription** from your nanocart widget plan. You need an active hosted plan to use it. | Plan | Price | Templates | Custom Domain | Branding | |---|---|---|---|---| | Starter | $7/mo ($70/yr) | Classic only | No | Always shown | | Standard | $15/mo ($150/yr) | Classic, Bold, Compact | No | Always shown | | Growth | $25/mo ($250/yr) | Classic, Bold, Compact | Yes | Always shown | | Scale | $49/mo ($490/yr) | Classic, Bold, Compact | Yes | Can be removed | Subscribe to or manage your hosted plan from **Admin Panel -> Billing**. --- ## Templates Templates control the visual design of your storefront. Choose based on your store's style: - **Classic** — Clean, minimal design with a neutral color palette. Recommended for most stores. The only template available on the Starter plan. - **Bold** — Large hero images, high-contrast typography. Best for lifestyle brands, apparel, and visually-driven products. - **Compact** — Dense product grid layout. Best for stores with a large number of products where browsing efficiency matters. To change your template, go to **Admin Panel -> Storefront -> Template** and select from the available options. Standard plan and above can access all three templates. Starter plan is locked to Classic. Template changes may take up to 60 seconds to appear due to CloudFront caching. If you don't see the change immediately, wait a moment and hard-refresh your storefront. --- ## Custom Domain Setup (Growth and Scale Plans) Growth and Scale plan subscribers can point any domain or subdomain to their hosted storefront. ### Steps 1. In the Admin Panel, go to **Storefront -> Custom Domain** and enter your domain (e.g., `shop.mybrand.com` or `store.mybrand.com`). 2. Save the setting. 3. In your DNS provider, create a **CNAME record** pointing your domain to: ``` dtgn99ynmfly1.cloudfront.net ``` 4. Wait for DNS propagation. This typically takes **5–60 minutes**, but can take up to 24 hours in rare cases. ### What to Know - **HTTPS is automatic.** nanocart provisions an SSL certificate through CloudFront — no certificate setup required on your end. - Your original `{storeId}.nanocart.io` URL **continues to work** alongside your custom domain. Both URLs are active at the same time. - To check DNS propagation status, use [dnschecker.org](https://dnschecker.org) and search for your custom domain. --- ## Branding (Scale Plan Only) All hosted storefronts display a "Powered by nanocart" label in the footer by default. This applies to all plans — Starter, Standard, and Growth — and cannot be removed on those plans. **Scale plan** subscribers can remove the footer branding: 1. Go to **Admin Panel -> Storefront** 2. Find the **Show "Powered by nanocart"** toggle 3. Set it to **off** (`showPoweredBy: false`) 4. Save If you are on a Scale plan and the branding is still showing, confirm the toggle is set to off and hard-refresh your storefront after a moment. --- ## Enabling and Disabling the Storefront To enable: go to **Admin Panel -> Storefront** and toggle **Storefront** on. You must have an active hosted plan subscription for this toggle to work. To disable: toggle it off. Visitors will see a "not available" message at your storefront URL. If your hosted plan subscription is canceled, the storefront is automatically disabled at the end of the billing period. --- ## Common Issues **My storefront URL shows a 404 or "not available" message.** Check two things: (1) the **Enabled** toggle in Admin Panel -> Storefront is on, and (2) you have an active hosted plan subscription. If your plan lapsed, the storefront is disabled automatically. **My custom domain isn't working after I set up the CNAME.** Verify the CNAME points exactly to `dtgn99ynmfly1.cloudfront.net` — not the API URL (`api.nanocart.io`) or any other address. Then check propagation at dnschecker.org. DNS can take up to 24 hours in rare cases. **My template change isn't showing up.** CloudFront caches storefront configuration for up to 60 seconds. Wait a moment, then do a hard-refresh (Ctrl+Shift+R on Windows/Linux, Cmd+Shift+R on Mac) on your storefront. **Can I use a custom domain on the Standard plan?** No. Custom domain support requires the Growth plan ($25/mo) or Scale plan ($49/mo). **Can I use both the widget on my existing site AND a hosted storefront?** Yes. The widget and hosted storefront are fully independent. You can embed the widget on any existing website while also running a hosted storefront at your `{storeId}.nanocart.io` URL. They share the same product catalog and inventory. **"Powered by nanocart" is still showing and I'm on Scale.** Go to Admin Panel -> Storefront and confirm the **Show "Powered by nanocart"** toggle is off. Save and hard-refresh. If the issue persists, contact support. **Can I change my storeId / subdomain?** No. The storeId is set at account creation and cannot be changed. If you need a different public-facing URL, add a custom domain (requires Growth or Scale plan). --- ## API Reference (for Developers) These endpoints power the hosted storefront SPA internally. They are available if you need to integrate or debug programmatically. - `GET /shop/{storeId}/storefront-config` — Returns all storefront settings. Called by the SPA on every page load. - `GET /shop/resolve-domain?domain={domain}` — Maps a custom domain to a storeId. Used by the SPA when running under a custom domain. - `GET /shop/{storeId}/admin/storefront` — Retrieve the current storefront configuration (authenticated). - `PUT /shop/{storeId}/admin/storefront` — Update storefront settings (authenticated). Base API URL: `https://api.nanocart.io` # ===== Storefront Customization ===== (https://docs.nanocart.io/#storefront-templates) # Storefront Customization This document covers every configurable field for nanocart hosted storefronts. A hosted plan is required to use a storefront. Settings can be changed in the **Admin Panel → Storefront** section at https://nanocart.io/admin, or via the API. --- ## Accessing and Updating Settings - **Admin Panel**: Navigate to https://nanocart.io/admin → Storefront - **GET current settings**: `GET /shop/{storeId}/admin/storefront` - **Update settings**: `PUT /shop/{storeId}/admin/storefront` Partial updates are supported — only include the fields you want to change. Example: ```json {"heroTitle": "Welcome to My Shop", "accentColor": "#e74c3c"} ``` --- ## Template The template controls your storefront's overall visual layout and style. - **`classic`** — Clean, minimal design. Recommended for most stores. Available on all hosted plans. - **`bold`** — Large images, high contrast. Best for apparel and lifestyle brands. - **`compact`** — Dense product grid. Best for stores with many products. **Plan note**: Starter plan customers are limited to the `classic` template. Standard plan or above is required to select `bold` or `compact`. --- ## Colors - **`accentColor`** — Primary brand color (hex, e.g. `"#418a9e"`). Applied to buttons, links, CTAs, and accent elements throughout the storefront. - **`backgroundColor`** — Page background color (hex, default `"#FFFFFF"`). - **`textColor`** — Body text color (hex, default `"#333333"`). All color values must be valid hex codes including the `#` prefix. --- ## Logo and Store Name - **`logo`** — URL of your store logo image. Recommended size: 240×72px, transparent PNG. Upload via the image upload flow described below. - **`showStoreName`** — Boolean. If `true`, the store name text appears next to the logo in the navigation bar. If `false`, only the logo image is shown. - **`favicon`** — URL of the browser tab icon. Must be square, minimum 32×32px. PNG or ICO format. Upload via the image upload flow. --- ## Hero Section The hero is the large banner at the top of your storefront home page. - **`heroTitle`** — Main heading text. Defaults to the store name if left empty. - **`storeDescription`** — Subtitle or description text displayed below the hero title. - **`heroLayout`** — Controls the layout of the hero section: - `"center"` — Text is centered, no image displayed. - `"image-left"` — Hero image on the left, text on the right. - `"image-right"` — Hero image on the right, text on the left. - **`heroImage`** — URL of the hero image. Only displayed when `heroLayout` is `"image-left"` or `"image-right"`. Ignored for `"center"` layout. Recommended size: 1440×720px landscape (JPG or WebP). --- ## About Page - **`aboutTitle`** — Heading displayed on the About page. Defaults to "About {storeName}" if empty. - **`aboutContent`** — Body text for the About page. Plain text only — HTML is not supported. Line breaks in the text are preserved when rendered. - **`aboutImage`** — Optional image displayed alongside the about page content. Recommended size: 800×600px. --- ## Announcement Bar A banner displayed at the top of every page on your storefront. Useful for promotions, limited-time offers, shipping notices, and seasonal messages. - **`announcementBar.enabled`** — Boolean. Shows (`true`) or hides (`false`) the announcement bar. - **`announcementBar.text`** — The message to display (e.g. `"Free shipping on orders over $50!"`). - **`announcementBar.backgroundColor`** — Hex color for the bar background. Defaults to the accent color. - **`announcementBar.textColor`** — Hex color for the bar text. Defaults to white (`"#FFFFFF"`). The announcement bar appears on every storefront page when enabled — this is expected behavior. --- ## Social Links Social icons appear in the storefront footer. Leave any field empty to hide that icon. - **`socialLinks.instagram`** — Handle or full URL - **`socialLinks.twitter`** — Handle or full URL - **`socialLinks.facebook`** — Handle or full URL - **`socialLinks.tiktok`** — Handle or full URL - **`socialLinks.youtube`** — Handle or full URL --- ## Branding - **`showPoweredBy`** — Boolean (default `true`). Controls whether "Powered by nanocart" appears in the footer. Can only be set to `false` on the **Scale hosting plan**. Setting this to `false` on Starter or Growth plans has no effect — the badge will still appear. --- ## Uploading Images Use this three-step flow to upload logos, hero images, about images, and favicons. 1. **Get a presigned URL**: `POST /shop/{storeId}/admin/upload-url` with body: ```json { "fileName": "my-logo.png", "contentType": "image/png", "uploadType": "storefront_image", "fileSizeMB": 0.5 } ``` 2. **Upload the file**: `PUT` the file to the presigned URL returned in step 1. Include the `Content-Type` header matching the file type. 3. **Use the URL**: Copy the `fileUrl` from the step 1 response and set it as the value for `logo`, `heroImage`, `aboutImage`, or `favicon`. **Presigned URLs expire after 15 minutes.** Complete the upload promptly after requesting one. **File size limits by plan:** - Free: 5 MB - Standard: 25 MB - Pro: 50 MB - Expert: 500 MB **Recommended image dimensions:** | Field | Dimensions | Format | |---|---|---| | `logo` | 240×72px | PNG (transparent background) | | `heroImage` | 1440×720px | JPG or WebP | | `aboutImage` | 800×600px | JPG, PNG, or WebP | | `favicon` | 32×32px or 64×64px | PNG or ICO | --- ## Caching and Propagation Storefront configuration is cached by CloudFront for approximately **60 seconds**. After saving changes in the admin panel or via the API, wait up to 60 seconds before the live storefront reflects the update. To verify changes faster, hard-refresh the storefront page: - **Mac**: Cmd+Shift+R - **Windows/Linux**: Ctrl+Shift+R --- ## Common Issues **My logo isn't showing.** Check that the logo URL is publicly accessible. If you just uploaded it, wait 30 seconds and hard-refresh. Use the exact `fileUrl` returned from the upload API — do not modify the URL. **Colors aren't updating on the live storefront.** CloudFront caches settings for up to 60 seconds. Wait and hard-refresh. **Can I add custom HTML or CSS?** Not currently supported. Customization is done through the provided fields only. **Can I add custom navigation links?** Not supported. Navigation is auto-generated from your store's active product categories. **My about page shows but the content is empty.** Add text to the `aboutContent` field in Admin Panel → Storefront → About section. **My hero image isn't showing.** The `heroImage` field is only displayed when `heroLayout` is set to `"image-left"` or `"image-right"`. With the `"center"` layout, no hero image is shown regardless of what URL is set. **The announcement bar appears on every page — is that a bug?** No, this is expected. The announcement bar is a site-wide banner and displays on all storefront pages. To hide it, set `announcementBar.enabled` to `false`. **"Powered by nanocart" still shows after I disabled it.** Removing the powered-by badge requires the **Scale hosting plan**. On Starter and Growth plans, the setting is ignored and the badge always displays. # ===== Stripe Setup ===== (https://docs.nanocart.io/#payments-stripe) # Stripe Setup NanoCart integrates directly with your Stripe account to process payments. You retain full control of your funds — NanoCart never holds or routes payments on your behalf. ## What You Need - A Stripe account ([stripe.com](https://stripe.com)). If you don't have one, create a free account. New accounts start in a **sandbox** (you'll see a banner: "You're testing in a sandbox") — that's fine for setup and testing; completing Stripe's business verification unlocks the live account for real payments. ## Answering Stripe's Setup Questions When you create a new Stripe account, Stripe asks a few onboarding questions. Here's how to answer them for NanoCart: - **"How do you want to get started?"** — choose **Accept payments** (sell products or services directly to your customers), then Continue. - **"How do you want to accept payments?"** — choose **Prebuilt checkout form**. NanoCart redirects your buyers to a Stripe-hosted checkout page, which is exactly what this option describes. Don't worry about getting these "wrong" — they only tailor Stripe's own setup guide and recommendations. Nothing is locked in, and NanoCart works the same regardless. If Stripe shows a question not listed here, choosing **Skip** is always safe. ## Do I Need to Add My Products to Stripe? **No.** NanoCart is your product catalog — you never create products in Stripe. At checkout, NanoCart sends Stripe each line item (name, price, quantity) on the fly, and Stripe simply processes the payment. Your Stripe transactions will still show the product names on every charge. Stripe's own onboarding and setup guide may prompt you to "add a product" or set up its Product catalog — **skip those steps**. That catalog is only for merchants selling through Stripe's own tools (payment links, invoices, subscriptions). Managing products in two places would just create conflicting prices; everything about a product lives in NanoCart only. ## Step-by-Step Setup **1. Log into your Stripe Dashboard** Go to [dashboard.stripe.com](https://dashboard.stripe.com) and sign in to the account you want to connect to your NanoCart store. **2. Copy your API keys — they're right on the Home page** 1. On the dashboard **Home** page, look for the **API keys** panel on the right side. It shows your **Secret key** and **Publishable key** with copy buttons. (If you don't see the panel, click **Go to API keys** or open [dashboard.stripe.com/apikeys](https://dashboard.stripe.com/apikeys).) 2. Copy the **Publishable key** (begins with `pk_test_` in a sandbox, `pk_live_` in the live account). 3. Copy the **Secret key** (begins with `sk_test_` or `sk_live_`). If you're in a sandbox you'll get `pk_test_`/`sk_test_` keys. For real payments, click **Switch to live account** (top-right of the sandbox banner) and copy the `pk_live_`/`sk_live_` keys from there. Keep your Secret key private. Never share it publicly or commit it to source control. **3. Create a webhook endpoint** NanoCart requires a webhook so Stripe can confirm successful payments back to your store. 1. Click **Developers** in the **bottom-left corner** of the Stripe Dashboard (it's a small button at the very bottom of the sidebar, not a sidebar menu item), then choose **Webhooks**. 2. Click **+ Add destination**. 3. Keep **Your account** selected. In the events search box, type `checkout`, expand **Checkout**, and check **checkout.session.completed**. Click **Continue**. 4. Choose **Webhook endpoint** as the destination type and click **Continue**. 5. Enter the following **Endpoint URL** (give the destination any name you like, e.g. "NanoCart Checkout"): ``` https://api.nanocart.io/shop/webhooks/stripe ``` 6. Click **Create destination**. 7. On the destination's detail page, reveal and copy the **Signing secret** (begins with `whsec_`). Note: a sandbox and the live account each have their own webhooks. Create the webhook in the **live account** for real payments (and optionally one in the sandbox for testing — each has its own signing secret). **4. Enter your credentials in NanoCart** 1. Open your NanoCart admin at [portal.nanocart.io](https://portal.nanocart.io). 2. Navigate to **Settings > Store Configuration**. 3. Paste your **Publishable key**, **Secret key**, and **Webhook signing secret** into the Stripe fields. 4. Click **Save**. Your store is now connected to Stripe and ready to accept payments. ## Live Mode vs. Test Mode NanoCart supports both Stripe environments. Toggle between them in **Settings > Store Configuration** using the Stripe **Test Mode** switch — test keys and live keys have their own separate fields. - **Test mode**: Use the sandbox keys (`pk_test_` / `sk_test_`) to place orders without real charges. Stripe provides [test card numbers](https://stripe.com/docs/testing#cards) for simulating successful payments and declines. - **Live mode**: Use live keys (`pk_live_` / `sk_live_`) to process real transactions. Your Stripe account must have completed business verification before live payments work — until then only the sandbox is available. Make sure you copy keys from the matching place in Stripe — sandbox keys come from the sandbox, live keys from the live account (**Switch to live account** in the top-right). ## What Stripe Handles - **Payment processing**: Credit and debit cards, with support for additional payment methods based on your Stripe account settings. - **Tax calculation**: If you have [Stripe Tax](https://stripe.com/tax) enabled on your Stripe account, NanoCart will automatically apply tax at checkout based on the buyer's location. ## Buyer Experience When a customer clicks a checkout button on your site or storefront, they are redirected to a Stripe-hosted Checkout page. Stripe handles all payment details, card validation, and 3D Secure authentication. After a successful payment, the buyer is returned to your store's order confirmation page. # ===== PayPal Setup ===== (https://docs.nanocart.io/#payments-paypal) # Setting Up PayPal Payments nanocart supports PayPal as a payment processor alongside Stripe. Store owners can accept card payments, PayPal payments, or both. Checkout buttons are shown only for processors that are actually configured — buyers never see a dead button. --- ## Overview of how PayPal works in nanocart When a buyer clicks "Pay with PayPal": 1. nanocart creates a PayPal order server-side and redirects the buyer to PayPal's hosted approval page. 2. The buyer approves the payment on PayPal. 3. PayPal redirects back to nanocart, which captures the payment and creates the order. 4. A backup PayPal webhook (`PAYMENT.CAPTURE.COMPLETED`) handles the rare case where the buyer closes the tab before being redirected back. No PayPal JavaScript SDK is embedded in your storefront — everything is handled server-side via redirect. --- ## Step 1: Create a PayPal Developer account and App 1. Go to [developer.paypal.com](https://developer.paypal.com) and sign in with your PayPal business account (or create one). 2. Click **Apps & Credentials** in the top navigation. 3. Make sure you're on the **Sandbox** tab (for testing) or **Live** tab (for production). **Start with Sandbox.** 4. Click **Create App**. 5. Give the app a name (e.g. "My Store — nanocart") and leave the App Type as **Merchant**. 6. Click **Create App**. 7. You'll land on the app detail page. You'll see: - **Client ID** — safe to share publicly - **Secret** — keep private, never share 8. Copy both values — you'll paste them into nanocart in Step 3. > **Sandbox vs. Live:** Sandbox credentials let you test with fake money using PayPal's test buyer accounts. Live credentials process real payments. They are completely separate apps with separate credentials. Always test in Sandbox first. --- ## Step 2: Set up the PayPal webhook PayPal sends a webhook notification when a payment is captured. nanocart uses this as a backup to create the order if the buyer closes the tab before being redirected back. 1. While still on the app detail page, scroll down to **Webhooks** and click **Add Webhook**. 2. Set the **Webhook URL** to: ``` https://api.nanocart.io/shop/webhooks/paypal ``` 3. Under **Event types**, check `PAYMENT.CAPTURE.COMPLETED` (you can search for it). 4. Click **Save**. 5. The webhook will now appear in the list. Click on it to view the detail page. 6. Copy the **Webhook ID** (it looks like `WH-XXXX...`). You'll need this in nanocart. > **Note:** This webhook URL is shared across all nanocart stores. nanocart routes events to the correct store using the order ID embedded in each PayPal order. --- ## Step 3: Connect PayPal to your nanocart store 1. Log in to your nanocart admin panel at [nanocart.io/admin](https://nanocart.io/admin). 2. Select your store, then go to **Settings**. 3. Scroll down to the **PayPal Configuration** card. 4. Fill in: - **PayPal Client ID** — paste the Client ID from Step 1 - **PayPal Secret** — paste the Secret from Step 1 - **PayPal Webhook ID** — paste the Webhook ID from Step 2 - **Mode** — select **Sandbox** for testing 5. Click **Save PayPal Config**. Your storefront will now show a "Pay with PayPal" button at checkout alongside any configured Stripe button. --- ## Step 4: Test with sandbox buyer accounts PayPal automatically creates sandbox business and personal test accounts for you. 1. Go to [developer.paypal.com/developer/accounts](https://developer.paypal.com/developer/accounts). 2. You'll see two pre-created accounts: one **Business** (your sandbox merchant) and one **Personal** (a test buyer). The Personal account has a balance pre-loaded. 3. Click the Personal account, then **View/Edit Account** to see the sandbox email and password. 4. In your storefront, add items to the cart and click **Pay with PayPal**. 5. On the PayPal approval page, log in with the sandbox Personal account credentials. 6. Complete the payment. 7. You should be redirected back to your storefront's success page, and the order should appear in the nanocart admin under **Orders**. > **Tip:** If the redirect doesn't complete (e.g., you close the tab after approving), the PayPal webhook should still finalize the order within a few seconds. --- ## Step 5: Go Live Once you've confirmed a successful end-to-end test in Sandbox: 1. Go back to [developer.paypal.com/dashboard/applications](https://developer.paypal.com/dashboard/applications) and switch to the **Live** tab. 2. Create a new app (same steps as Step 1) — or select an existing live app if you already have one. 3. Copy the **Live Client ID** and **Live Secret**. 4. Set up a new webhook on the Live app (same URL, same event) and copy the **Live Webhook ID**. 5. In the nanocart admin **PayPal Configuration** card, replace all three fields with the live values. 6. Change **Mode** to **Live**. 7. Click **Save PayPal Config**. > **Important:** Live and Sandbox credentials are completely separate. You must update all three fields (Client ID, Secret, Webhook ID) when switching modes. Never use Sandbox credentials in Live mode or vice versa. --- ## Billing history and order records All orders processed via PayPal appear in the nanocart admin **Orders** page alongside Stripe orders. Each order shows a **PayPal** or **Stripe** badge so you can see at a glance which processor handled it. The customer order confirmation email also shows "Paid via PayPal" or "Paid via Stripe." --- ## Frequently asked questions **Can I use PayPal without Stripe?** Yes. If you only configure PayPal (no Stripe keys), only the "Pay with PayPal" button will appear at checkout. If you configure both, both buttons appear and the buyer chooses. **What currencies does PayPal support?** PayPal supports most major currencies. Zero-decimal currencies (e.g. JPY) are handled correctly by nanocart. If PayPal rejects a currency, the checkout will return an error. **Does nanocart compute tax for PayPal orders?** Yes, but using the store's default tax rate rather than destination-based tax (which PayPal doesn't support at order-creation time). Tax may differ slightly between Stripe and PayPal checkouts for stores with destination-based tax configured. **What happens if the buyer closes the tab after approving on PayPal?** The PayPal webhook (`PAYMENT.CAPTURE.COMPLETED`) will capture the payment and create the order within a few seconds. The buyer will receive their confirmation email, and the order will appear in the admin. If the buyer returns to the success URL later, they'll see their existing order — there's no risk of a double-capture. **What is the Webhook ID used for?** nanocart uses the Webhook ID to cryptographically verify that incoming webhook notifications are genuinely from PayPal and from your specific app. Without it, the backup webhook path won't work. **My PayPal button doesn't appear at checkout.** Make sure all three fields (Client ID, Secret, Webhook ID) are saved in the PayPal Configuration card and Mode is set correctly. Checkout buttons are only shown for processors where both the Client ID and Secret are configured on the server. # ===== Orders ===== (https://docs.nanocart.io/#orders-management) # Orders — nanocart Support Knowledge Base nanocart is a SaaS e-commerce platform. Store owners connect their own Stripe account for payments. Orders are processed through Stripe Checkout and managed via the nanocart admin panel at https://nanocart.io/admin. The API base is https://api.nanocart.io. --- ## Order Lifecycle Orders move through the following states in sequence: - **pending** — A Stripe Checkout session has been created. The customer has started checkout but payment has not yet been confirmed. No inventory is decremented at this stage. - **paid** — Stripe fires the `checkout.session.completed` webhook to nanocart. The order is marked paid and inventory is decremented automatically. - **shipped** — The store owner manually updates the order via the admin panel or API, typically adding a tracking number. This triggers an `order.shipped` webhook to any configured webhook endpoint. - **delivered** — The store owner manually updates the order to indicate delivery. - **cancelled** — Set manually by the store owner (e.g., after issuing a Stripe refund) or automatically when payment fails. Inventory is restored when an order is cancelled. --- ## Order Object Fields Each order contains the following fields: - **orderId** — UUID, unique identifier for the order. - **orderNumber** — Human-readable identifier in `PREFIX-NUMBER` format (e.g., `MYS-1001`). The prefix is set in Settings under `order_config`. - **email** — Customer's email address. Used for order confirmation emails and as a verification token for public order lookup. - **customerName** — Customer name collected during Stripe Checkout. - **items** — Array of line items. Each item includes: `productId`, `name`, `variantName`, `price` (cents), `quantity`. - **subtotal** — Pre-tax, pre-shipping total in cents. - **shippingCost** — Shipping charge in cents. - **taxAmount** — Tax collected in cents. - **discountAmount** — Any discount applied, in cents. - **total** — Final amount charged to the customer in cents. - **shippingAddress** — Object with `name`, `line1`, `city`, `state`, `zip`, `country`. - **status** — Current order state: `pending`, `paid`, `shipped`, `delivered`, or `cancelled`. - **trackingNumber** — Shipping tracking number (optional, added by store owner). - **notes** — Internal notes visible only to the store owner, not the customer. - **createdAt** — ISO 8601 timestamp of when the order was created. --- ## Order Configuration Order numbers are configured in **Settings → order_config**: - **orderPrefix** — A short string (e.g., `MYS`) prepended to each order number. - **nextOrderNumber** — Auto-incrementing integer. The resulting order number format is `PREFIX-NUMBER` (e.g., `MYS-1001`, `MYS-1002`, and so on). --- ## Fulfillment Flow (Step by Step) 1. Customer adds items to cart and initiates checkout. nanocart creates a Stripe Checkout session and redirects the customer to Stripe. 2. Customer completes payment on Stripe's hosted checkout page. 3. Stripe fires the `checkout.session.completed` webhook to nanocart. nanocart sets the order status to **paid** and decrements inventory for each purchased item. 4. The store owner sees the new order appear on the **Orders** page in the admin panel (https://nanocart.io/admin). 5. The store owner packs and ships the order. 6. The store owner updates the order via `PUT /shop/{storeId}/admin/orders/{orderId}` with `status: "shipped"` and the `trackingNumber`. 7. nanocart fires an `order.shipped` webhook to the store's configured webhook endpoint (if set up). 8. The customer can look up their order status at the store's site using their `orderId` and email address. --- ## Monthly Order Limits by Tier | Tier | Monthly Order Limit | |----------|---------------------| | Free | 5 | | Standard | 500 | | Pro | 5,000 | | Expert | Unlimited | Limits reset on the **1st of each month**. When a store exceeds its limit, checkout attempts return a `403` error with the message: `"This store has reached its monthly order limit"`. Upgrading the store's plan immediately restores capacity. --- ## API Endpoints ### List Orders (Admin) ``` GET /shop/{storeId}/admin/orders ``` Query parameters: - `status` — Filter by order status. - `from` / `to` — ISO date range filter. - `limit` — Number of results (default: 50). - `lastKey` — Pagination cursor for the next page. ### Update Order (Admin) ``` PUT /shop/{storeId}/admin/orders/{orderId} ``` Request body (all fields optional): - `status` — New status string. - `trackingNumber` — Shipping tracking number. - `notes` — Internal notes. ### Public Order Lookup ``` GET /shop/{storeId}/orders/{orderId}?email=customer@example.com ``` Requires the exact customer email on file. Email acts as a verification token. Returns order details including status and tracking number. ### Reports ``` GET /shop/{storeId}/admin/reports ``` Returns revenue totals, order count, average order value, tax collected by state, and top-selling products. --- ## Order Emails nanocart sends order confirmation emails using the store's **email_config** settings (configured in Settings): - **fromEmail** — The sender email address. - **fromName** — The display name for the sender. Emails are sent via Amazon SES and include order details, itemized list, totals, and tracking number when available. --- ## Common Issues ### Order shows as pending but payment went through in Stripe The Stripe webhook has not delivered or is pointing to the wrong URL. To diagnose: 1. Go to **Stripe Dashboard → Developers → Webhooks**. 2. Check the delivery logs for failed or undelivered events. 3. Confirm the webhook endpoint URL is set to: `https://api.nanocart.io/shop/{storeId}/webhook` (replace `{storeId}` with the store's actual ID). 4. Re-send the `checkout.session.completed` event from Stripe if needed. ### Customer didn't receive a confirmation email - Verify **fromEmail** and **fromName** are set correctly in Settings under `email_config`. - Ask the customer to check their spam or junk folder — SES emails occasionally get filtered. - Confirm the email address on the order is correct. ### Inventory not decreasing after purchase Inventory is decremented when the Stripe webhook fires and sets the order status to **paid**. If the webhook is not working, inventory will not update. Resolve the webhook issue first (see above), then check inventory counts. ### How to issue a refund nanocart does not process refunds directly. To refund a customer: 1. Issue the refund in the **Stripe Dashboard** against the original charge. 2. Manually update the order status to **cancelled** in the nanocart admin panel. This restores inventory. ### Checkout session expired Stripe Checkout sessions expire after approximately 30 minutes of inactivity. The customer must restart the checkout process from the beginning. There is no way to resume an expired session. ### Store has reached its monthly order limit The store's current tier has hit its monthly cap. Options: - Upgrade the plan immediately to restore capacity. - If the store is on Expert tier and still seeing this error, contact nanocart support. # ===== Shipping ===== (https://docs.nanocart.io/#shipping-overview) # Shipping Configuration — nanocart Support Knowledge Base nanocart supports five shipping methods, a free-shipping threshold that works with any method, and the ability to offer local pickup alongside standard shipping. All shipping settings live under the `settings.shipping_config` object and are updated via a single API call. --- ## Shipping Methods ### 1. flat_rate A single fixed cost applied to every order regardless of the number of items or total weight. Set `flatRate` in cents (e.g., `499` = $4.99). This is the simplest method and works well for most small stores where a one-size-fits-all charge is fair. **Example config:** ```json { "method": "flat_rate", "flatRate": 499, "freeShippingEnabled": false, "freeShippingThreshold": 0, "defaultItemShippingCost": 0, "tiers": [], "localPickupEnabled": false, "localPickupInstructions": "" } ``` --- ### 2. per_item Charges a shipping cost per unit in the cart. Each product has its own `shippingCost` field (in cents) set in the admin panel. If a product does not have `shippingCost` set, nanocart falls back to `defaultItemShippingCost`. **Calculation:** Total shipping = sum of (`product.shippingCost` × quantity) for every line item in the cart. If a product is missing a `shippingCost` value and `defaultItemShippingCost` is also 0, that product contributes $0.00 to shipping. Always set a `defaultItemShippingCost` as a safety net. **Requires Standard plan or higher.** **Example config:** ```json { "method": "per_item", "flatRate": 0, "freeShippingEnabled": false, "freeShippingThreshold": 0, "defaultItemShippingCost": 299, "tiers": [], "localPickupEnabled": false, "localPickupInstructions": "" } ``` --- ### 3. tiered Shipping cost varies by order subtotal. Define a `tiers` array where each tier specifies a subtotal range (`minAmount` and `maxAmount`, both in cents) and a `rate` (in cents). Set `maxAmount` to `null` on the final tier to mean "this amount and above." A `rate` of `0` means free shipping for that bracket. **Requires Standard plan or higher.** **Example config (free shipping over $75):** ```json { "method": "tiered", "flatRate": 0, "freeShippingEnabled": false, "freeShippingThreshold": 0, "defaultItemShippingCost": 0, "tiers": [ { "minAmount": 0, "maxAmount": 2500, "rate": 699 }, { "minAmount": 2500, "maxAmount": 5000, "rate": 499 }, { "minAmount": 5000, "maxAmount": 7500, "rate": 299 }, { "minAmount": 7500, "maxAmount": null, "rate": 0 } ], "localPickupEnabled": false, "localPickupInstructions": "" } ``` --- ### 4. free Always free shipping for all orders. No additional fields need to be configured. Available on all plans including Free. **Example config:** ```json { "method": "free", "flatRate": 0, "freeShippingEnabled": false, "freeShippingThreshold": 0, "defaultItemShippingCost": 0, "tiers": [], "localPickupEnabled": false, "localPickupInstructions": "" } ``` --- ### 5. local_pickup The customer picks up the order in person; no shipping charge is applied. Set `localPickupInstructions` to a string containing your address, hours, and any pickup instructions — this text is shown to the customer at checkout. **Requires Standard plan or higher.** **Example config:** ```json { "method": "local_pickup", "flatRate": 0, "freeShippingEnabled": false, "freeShippingThreshold": 0, "defaultItemShippingCost": 0, "tiers": [], "localPickupEnabled": true, "localPickupInstructions": "Pick up at 123 Main St, Suite 4. Hours: Mon–Fri 10am–5pm. Please bring your order confirmation email." } ``` --- ## Free Shipping Threshold `freeShippingEnabled` and `freeShippingThreshold` work alongside any active shipping method. When enabled and the order subtotal meets or exceeds `freeShippingThreshold` (in cents), shipping is free — regardless of which method is active. **Example:** `flat_rate` at $4.99 with free shipping over $50: ```json { "method": "flat_rate", "flatRate": 499, "freeShippingEnabled": true, "freeShippingThreshold": 5000, "defaultItemShippingCost": 0, "tiers": [], "localPickupEnabled": false, "localPickupInstructions": "" } ``` Orders under $50 pay $4.99. Orders of $50 or more ship free. --- ## Combining Shipping and Local Pickup To offer customers a choice between shipping and local pickup at checkout, set your preferred shipping `method` (e.g., `flat_rate`) AND set `localPickupEnabled: true`. Both options are presented simultaneously at checkout; the customer selects one. **Example config:** ```json { "method": "flat_rate", "flatRate": 599, "freeShippingEnabled": false, "freeShippingThreshold": 0, "defaultItemShippingCost": 0, "tiers": [], "localPickupEnabled": true, "localPickupInstructions": "Pick up at 456 Ocean Ave. Hours: Tue–Sat 11am–6pm." } ``` --- ## Updating Shipping Configuration Send the complete `shipping_config` object — not just changed fields — in a `PUT` request to: ``` PUT https://api.nanocart.io/shop/{storeId}/admin/settings ``` **Request body:** ```json { "settingKey": "shipping_config", "value": { "method": "flat_rate", "flatRate": 499, "freeShippingEnabled": true, "freeShippingThreshold": 5000, "defaultItemShippingCost": 0, "tiers": [], "localPickupEnabled": false, "localPickupInstructions": "" } } ``` Partial updates are not supported. Always include all fields even if they are not relevant to the active method. --- ## Plan Restrictions | Method | Free | Standard | Pro | Expert | |---|---|---|---|---| | flat_rate | Yes | Yes | Yes | Yes | | free | Yes | Yes | Yes | Yes | | per_item | No | Yes | Yes | Yes | | tiered | No | Yes | Yes | Yes | | local_pickup | No | Yes | Yes | Yes | Attempting to set `per_item`, `tiered`, or `local_pickup` on a Free plan returns a `TIER_SHIPPING_RESTRICTED` error. The customer must upgrade to Standard or higher to use these methods. Direct them to the Billing section of the admin panel at `https://nanocart.io/admin`. --- ## Common Questions **"How do I offer free shipping on orders over $50?"** Keep your current shipping method and set `freeShippingEnabled: true` with `freeShippingThreshold: 5000`. This works with flat_rate, per_item, tiered, or any other method. **"Can I offer both shipping and local pickup at the same time?"** Yes. Set your shipping method (e.g., `flat_rate`) for the shipping option and also set `localPickupEnabled: true`. Customers will see both choices at checkout. **"Per-item shipping isn't calculating correctly."** Open the admin panel and check each product's `shippingCost` field. Products with no value set fall back to `defaultItemShippingCost`. If that is also 0, those products contribute $0 to shipping. Set a non-zero `defaultItemShippingCost` to catch products missing individual shipping costs. **"I'm on the Free plan and can't select per_item."** That's expected. The `per_item`, `tiered`, and `local_pickup` methods require a Standard plan or higher. Upgrade in the Billing section of the admin panel. **"How do I set up tiered shipping with free shipping over $75?"** Use the `tiered` method and include a final tier with `minAmount: 7500`, `maxAmount: null`, `rate: 0`. The `null` maxAmount means "this amount and above," and a rate of `0` means free shipping for that bracket. **"Do I need to resend all fields when updating shipping?"** Yes. The `PUT /shop/{storeId}/admin/settings` endpoint replaces the entire `shipping_config` object. Always send the full config, including fields not relevant to your active method, to avoid unintentionally clearing values. # ===== Coupons ===== (https://docs.nanocart.io/#coupons-overview) # Coupons Coupons let merchants offer discounts at checkout. Customers enter a code in the cart widget, and nanocart validates and applies the discount before sending the order to Stripe. ## Coupon Types **percent_off** — Subtracts a percentage from the order subtotal. The `value` field is an integer from 1 to 100. Example: `value: 20` takes 20% off. **fixed_amount** — Subtracts a flat dollar amount from the order subtotal. The `value` field is in cents. Example: `value: 500` takes $5.00 off. **free_shipping** — Zeroes out the shipping cost entirely. The `value` field is ignored. The validation response returns `discountAmount: 0`, but the Stripe checkout session is updated to remove shipping fees. ## Coupon Fields | Field | Type | Description | |---|---|---| | `code` | string | The code customers enter at checkout. Auto-normalized to uppercase — "save20" is stored and matched as "SAVE20". | | `type` | string | `"percent_off"`, `"fixed_amount"`, or `"free_shipping"` | | `value` | integer | Percentage (percent_off) or cents (fixed_amount). Ignored for free_shipping. | | `minOrderAmount` | integer \| null | Optional minimum subtotal in cents required to use the coupon. | | `maxUses` | integer \| null | Optional cap on total uses across all customers. `null` means unlimited. | | `currentUses` | integer | Read-only. Incremented by the server on each successful use. | | `expiresAt` | ISO datetime \| null | Optional expiration. `null` means the coupon never expires. | | `status` | string | `"active"` or `"inactive"`. The DELETE endpoint sets this to inactive — it does not permanently delete the record. | ## Tier Limits | Plan | Max Coupons | |---|---| | Free | 1 | | Standard | 5 | | Pro | 10 | | Expert | Unlimited | **Important:** Both active and inactive coupons count toward the limit. Deactivating a coupon does not free up a slot. ## CRUD Operations **Create** — `POST /shop/{storeId}/admin/coupons` **Update** — `PUT /shop/{storeId}/admin/coupons` — `code` is required to identify the record; any other field can be updated, including `status`. **Deactivate** — `DELETE /shop/{storeId}/admin/coupons/{code}` — Sets `status` to `"inactive"`. Does not permanently delete. **List all** — `GET /shop/{storeId}/admin/coupons` ## Checkout Validation Flow When a customer enters a code at checkout, the widget calls: ``` POST /shop/{storeId}/coupons/validate { "code": "SAVE20", "subtotal": 4500 } ``` Response: ```json { "valid": true, "type": "percent_off", "value": 20, "discountAmount": 900, "message": "" } ``` `discountAmount` is calculated server-side based on the coupon type and current subtotal. For `free_shipping` coupons, `discountAmount` is `0` but shipping is removed from the Stripe session. ### Validation Error Messages | Error | Cause | |---|---| | `"This coupon has expired"` | `expiresAt` is in the past | | `"This coupon has reached its usage limit"` | `currentUses >= maxUses` | | `"Minimum order amount not met"` | Subtotal is below `minOrderAmount` | | `"Invalid coupon code"` | Code not found, or `status` is `"inactive"` | ## Example: Creating Each Coupon Type **Percent off (20% off any order)** ```json { "code": "SAVE20", "type": "percent_off", "value": 20 } ``` **Fixed amount ($10 off orders over $50)** ```json { "code": "10OFF", "type": "fixed_amount", "value": 1000, "minOrderAmount": 5000 } ``` **Free shipping (limited to 100 uses, expires end of year)** ```json { "code": "FREESHIP", "type": "free_shipping", "value": 0, "maxUses": 100, "expiresAt": "2026-12-31T23:59:59Z" } ``` **Single-use coupon** ```json { "code": "WELCOME1", "type": "percent_off", "value": 15, "maxUses": 1 } ``` ## Common Questions **How do I create a single-use coupon?** Set `maxUses: 1`. The coupon will reject any attempt after the first successful use. **Can I target a coupon to a specific product?** Not currently. Coupons apply to the full order subtotal. Product-level discounts are not supported. **Can I make a coupon that never expires?** Yes. Omit `expiresAt` or set it to `null`. **Are coupon codes case-insensitive for customers?** Yes. All codes are normalized to uppercase on creation and on lookup, so customers can type in any case and it will still match. **Why can't I create another coupon?** You've hit your plan's coupon limit. Free allows 1, Standard allows 5, Pro allows 10. Note that inactive coupons still count. To free up a slot, upgrade your plan — deactivating a coupon does not reduce your count. **How do I disable a coupon without deleting it?** Call the DELETE endpoint. This sets the coupon's `status` to `"inactive"` and stops it from being accepted at checkout, but the record is preserved. To re-enable it, call PUT with `status: "active"`. **What is `discountAmount` in the validation response?** It's the exact dollar amount (in cents) to subtract from the subtotal, calculated server-side. For example, a 20% coupon on a $45.00 order returns `discountAmount: 900`. For `free_shipping` coupons, this value is always `0` — the discount is applied to the shipping line in Stripe, not the subtotal. # ===== Tax Configuration ===== (https://docs.nanocart.io/#settings-tax) # Tax Configuration NanoCart supports two ways to collect sales tax, controlled from **Settings > Sales Tax**, plus a master toggle that turns collection off entirely. ## Settings shape | Field | Type | Meaning | |---|---|---| | `enabled` | boolean | Master toggle. Off = no tax on any order (Stripe or PayPal). | | `useStripeTax` | boolean | Stripe checkouts use Stripe Tax (automatic, jurisdiction-exact). | | `defaultRate` | number (percent) | Flat rate, e.g. `6` = 6%. Used for all PayPal orders, and for Stripe orders when `useStripeTax` is off. Clamped 0–30. | ```json PUT /shop/{storeId}/admin/settings { "settingKey": "tax_config", "value": { "enabled": true, "useStripeTax": false, "defaultRate": 6 } } ``` ## Option 1 — Stripe Tax (automatic) Stripe computes exact destination-based tax (state + county + city + special districts) from the buyer's address and your registered jurisdictions. Requires activating Stripe Tax in the Stripe Dashboard (Tax > Registrations) — Stripe charges 0.5% per taxed transaction and provides filing-grade reports. If Stripe Tax isn't activated, checkout returns a clear error telling the merchant to activate it or switch modes. ## Option 2 — Flat rate A single percentage applied to the post-discount subtotal. Always used for PayPal (PayPal cannot compute destination-based tax) and used for Stripe checkouts when `useStripeTax` is off. Stripe flat-rate orders attach a Stripe Tax Rate object so coupons interact correctly and the tax shows on Stripe receipts. ## Reporting Orders store `taxAmount`, `taxState` (from Stripe Tax jurisdiction or the shipping address), and `taxBreakdown` (Stripe Tax only). The Reports page shows total tax collected and a by-state table for any date range; orders without an address bucket under "Other / no address". These are collected amounts — remittance and filing are the merchant's responsibility. ## Notes - Per-product `taxable` flags and `stateRates` tables from the legacy system are no longer used. - New stores default to `enabled: false` — merchants opt in when they have nexus. # ===== Reports And Analytics ===== (https://docs.nanocart.io/#analytics-overview) # Reports and Analytics ## Overview nanocart provides built-in sales reporting accessible from both the Admin Panel and directly via API. Reports cover revenue, order volume, average order value, tax collection, shipping, discounts, and top products. All reporting is based on order data stored in nanocart's database — not your Stripe account balance. Reports are generated on-demand from stored order history. There is no real-time dashboard; data reflects orders as they exist in nanocart at the time of the request. --- ## Admin Panel Reports Page The Reports page at [https://nanocart.io/admin](https://nanocart.io/admin) provides a visual dashboard showing all key metrics: - A date picker to set your reporting period (from/to) - Total revenue, order count, and average order value - Total tax collected, shipping collected, and discounts applied - Tax breakdown by US state - Top products ranked by revenue **Note:** There is no export button on the Reports page. To get raw data for spreadsheets or further analysis, use the API endpoint described below. --- ## Reports API Endpoint **Endpoint:** `GET /shop/{storeId}/admin/reports` **Base URL:** `https://api.nanocart.io` **Auth:** Requires `x-api-key` header with your store API key ### Query Parameters | Parameter | Default | Description | |-----------|---------|-------------| | `from` | `2020-01-01` | Start of reporting period (ISO datetime string) | | `to` | Current time | End of reporting period (ISO datetime string) | | `status` | `paid,shipped,delivered` | Comma-separated order statuses to include | **Example request:** ``` GET /shop/my-store/admin/reports?from=2026-06-01T00:00:00Z&to=2026-06-30T23:59:59Z x-api-key: sc_live_... ``` ### Response Fields All monetary values are in **cents** — divide by 100 to get dollar amounts. - **`period`** — Object with `from` and `to` showing the actual reporting period used - **`totalRevenue`** — Sum of all order `total` fields for matching orders (cents) - **`totalOrders`** — Count of matching orders - **`averageOrderValue`** — `totalRevenue` divided by `totalOrders` (cents) - **`totalTaxCollected`** — Sum of all order `taxAmount` fields (cents) - **`taxByState`** — Object mapping 2-letter US state codes to total tax collected from that state (cents). Example: `{"SC": 18900, "NC": 6200}` means $189.00 collected from South Carolina and $62.00 from North Carolina - **`totalShipping`** — Sum of all order `shippingCost` fields (cents) - **`totalDiscounts`** — Sum of all order `discountAmount` fields (cents) - **`topProducts`** — Array of `{productId, name, unitsSold, revenue}` sorted by revenue descending --- ## Common Use Cases - **Monthly revenue** — Set `from` and `to` to cover the desired month (e.g., `from=2026-05-01T00:00:00Z&to=2026-05-31T23:59:59Z`) - **Quarterly tax reporting** — Use `taxByState` for state-by-state tax filings; each value is in cents, divide by 100 for dollar amounts - **Best-selling products** — Use `topProducts` to see what's driving the most revenue - **Period comparison** — Query the endpoint twice with different date ranges and compare the results - **Daily or weekly breakdowns** — Make multiple API calls with `from`/`to` covering each day or week and aggregate the results client-side --- ## Monthly Order Count for Tier Limits The Reports endpoint is separate from tier limit tracking. The live monthly order count used for tier enforcement comes from: ``` GET /shop/{storeId}/admin/tier ``` The order count is at `usage.monthlyOrders` in the response. This counter **resets on the 1st of each calendar month** regardless of your billing cycle. The Reports endpoint can also count orders for a given month using `from`/`to` parameters, but the `tier` endpoint is the authoritative source for limit enforcement. --- ## Limitations - **No customer-level analytics** — No lifetime value, repeat purchase rate, or customer segmentation - **No real-time data** — Reports are generated on-demand from stored order history - **No Google Analytics integration** — nanocart's admin panel uses a GA tag (G-GZS7118TFF) for nanocart's own internal usage tracking only; this is not your store's GA - **Date filtering only** — Filtering is by order `createdAt` timestamp; no filtering by product, category, or customer **Coming soon:** Cart analytics with session-level funnel tracking (pageview → add to cart → checkout → purchase) is planned as a future feature and will be available to all tiers at no additional charge. --- ## Common Questions **My revenue in nanocart doesn't match my Stripe balance.** Stripe may include nanocart platform subscription fees in your balance, and Stripe refunds may not be reflected in nanocart's order data. nanocart reports show the total charged to customers for store orders only. **Tax collected seems wrong.** - Tax not showing? Confirm **Settings > Sales Tax** is enabled and either Stripe Tax is active or a flat rate is set. **topProducts shows no results.** If there are no paid orders in the selected date range, `topProducts` will be empty. Try widening the date range. **I want to include cancelled orders in the report.** Add `"cancelled"` to the status parameter: `?status=paid,shipped,delivered,cancelled` **I need to see pending orders in the report.** Add `"pending"` to the status parameter: `?status=paid,shipped,delivered,pending` **I need daily or weekly revenue breakdowns.** Make multiple API calls with `from`/`to` covering each day or week and aggregate the results on your end. **Can I export reports to CSV or Excel?** There is no native export. Use the API endpoint and process the JSON in your own tools — Google Sheets (`IMPORTDATA`), Excel Power Query, or any scripting language can consume the JSON response. # ===== API And Authentication ===== (https://docs.nanocart.io/#settings-api) # nanocart API and Authentication Guide ## Overview The nanocart API is a REST API that accepts and returns JSON. All communication happens over HTTPS. The base URL for every request is: ``` https://api.nanocart.io ``` All endpoints fall into one of two categories: - **Public endpoints** — no authentication required; safe to call from a browser or storefront - **Admin endpoints** — require an `x-api-key` header; must only be called from your server --- ## Public Endpoints Public endpoints are designed to be called directly from your storefront or browser. No API key is needed. | Method | Path | Description | |---|---|---| | `GET` | `/shop/{storeId}/products` | List all active products | | `GET` | `/shop/{storeId}/products/{slug}` | Get a single product by slug | | `GET` | `/shop/{storeId}/categories` | List all categories | | `POST` | `/shop/{storeId}/coupons/validate` | Validate a coupon code | | `POST` | `/shop/{storeId}/checkout` | Create a Stripe checkout session | | `GET` | `/shop/{storeId}/orders/{orderId}?email=` | Look up an order (email verification required) | | `GET` | `/shop/{storeId}/storefront-config` | Retrieve hosted storefront settings | | `GET` | `/shop/resolve-domain?domain=` | Resolve a custom domain to a storeId | The `{storeId}` in every path is your store's string identifier — all lowercase, alphanumeric, and may contain hyphens (e.g., `my-shop`). It is **not** a UUID. --- ## Admin Endpoints Any route containing `/admin/` requires authentication. There are two URL patterns: - `/shop/{storeId}/admin/` — store-level operations (products, orders, coupons, settings, webhooks, etc.) - `/shop/admin/stores/{storeId}/` — platform-level operations (subscription management, store registration, API key regeneration) Admin endpoints must **never** be called from browser-side code, mobile app code, or any client-accessible environment. --- ## Your API Key **Where to find it:** Admin Panel → Settings → API Key **Format:** `sc_live_` followed by 64 lowercase hex characters. ``` sc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` **How to use it:** Add an `x-api-key` header to every admin API request. ```bash curl https://api.nanocart.io/shop/my-store/admin/products \ -H "x-api-key: sc_live_your_key_here" ``` ### Security Warning Your API key grants **full admin access** to your store — it can create and delete products, view all orders, and modify all settings. Never put it in: - Browser-side JavaScript (including your storefront or widget config) - Mobile app code - Any file that is publicly accessible or checked into a public repository Always make admin API calls from your server backend, where the key is stored in an environment variable or secrets manager. --- ## Regenerating Your API Key To regenerate your key: - **Admin Panel:** Admin Panel → Settings → Regenerate API Key - **API:** `POST /shop/admin/stores/{storeId}/regenerate-key` The old key is **immediately and permanently invalidated** the moment a new one is issued. There is no grace period. Update every integration that uses the old key before or immediately after regenerating. --- ## CORS and Allowed Domains By default, any origin may call your public API endpoints. For tighter security, you can restrict access to a specific list of domains: - **Setting location:** Admin Panel → Settings → Allowed Domains - **Format:** an array of domain strings, maximum 10 entries (e.g., `["mystore.com", "www.mystore.com"]`) Behavior when `allowedDomains` is configured: - Public API requests from a domain **not** in the list return `403 DOMAIN_NOT_ALLOWED` - The nanocart widget running on your site also needs your domain listed - Admin API calls made server-side are **not** subject to CORS domain restrictions - Leave the list empty to allow all origins (appropriate for development or unrestricted stores) --- ## Request Format Conventions - Set `Content-Type: application/json` on all `POST` and `PUT` requests - `{storeId}` is a lowercase string ID, not a UUID - Filtering, sorting, and pagination use query parameters on `GET` endpoints - **Pagination:** list endpoints return a `lastKey` field; pass it as `?lastKey=` to fetch the next page; default page size is 50 --- ## Data Conventions - **Money:** all monetary values are integers in **cents** — divide by 100 for display ($49.99 = `4999`) - **Timestamps:** ISO 8601 UTC strings (e.g., `2026-06-14T18:30:00Z`) - **Booleans:** JSON `true`/`false`, never the strings `"true"` or `"false"` - **Status fields:** lowercase strings — `"active"`, `"draft"`, `"paid"`, etc. --- ## Rate Limiting The API is rate-limited per store. When the limit is exceeded, the API returns: - **HTTP status:** `429 Too Many Requests` - **Response header:** `Retry-After: ` Back off for the number of seconds specified in `Retry-After` before retrying. Do not retry immediately in a tight loop. --- ## Error Response Format All errors return a JSON body with at minimum an `"error"` field. Most also include a `"code"` field. Tier-limit errors additionally include `"tierLimit": true`. ```json { "error": "Free plan allows 2 active products. Upgrade to Standard for 25.", "code": "TIER_PRODUCT_LIMIT", "tierLimit": true } ``` ### All Error Codes | Code | HTTP Status | Meaning | |---|---|---| | `INVALID_API_KEY` | 401 | Key does not exist or is invalid | | `STORE_SUSPENDED` | 403 | This store has been suspended | | `INVALID_INPUT` | 400 | Missing or malformed request data | | `STRIPE_NOT_CONFIGURED` | 400 | Store has no Stripe keys set up | | `STRIPE_INVALID_KEYS` | 400 | Stripe key format is incorrect | | `STRIPE_AUTH_ERROR` | 400 | Stripe rejected the keys | | `DOMAIN_NOT_ALLOWED` | 403 | Request origin is not in `allowedDomains` | | `TIER_PRODUCT_LIMIT` | 403 | Product count exceeds plan limit | | `TIER_ORDER_LIMIT` | 403 | Order count exceeds plan limit | | `TIER_COUPON_LIMIT` | 403 | Coupon count exceeds plan limit | | `TIER_UPLOAD_LIMIT` | 403 | Upload size or count exceeds plan limit | | `TIER_SHIPPING_RESTRICTED` | 403 | Shipping method not available on current plan | | `DUPLICATE_COUPON` | 409 | A coupon with that code already exists | --- ## Troubleshooting Common Issues ### Getting 401 Invalid API Key - Verify the `x-api-key` header is present on the request (not in the URL or body) - Confirm the full key value starts with `sc_live_` — partial keys or keys with extra whitespace will fail - Check whether the key was recently regenerated; the old key is immediately invalidated upon regeneration and must be replaced in all integrations ### Getting 403 DOMAIN_NOT_ALLOWED - Go to Admin Panel → Settings → Allowed Domains and add your domain to the list - Alternatively, clear the list entirely to allow requests from any origin - Confirm the domain is listed exactly as the browser sends it (e.g., `mystore.com` and `www.mystore.com` are treated as distinct entries) ### Getting a CORS error when calling an admin endpoint from the browser - Admin endpoints intentionally do not include CORS headers — they are server-side-only - Move the admin API call to your server backend and have your frontend call your own server instead - This is a security design decision, not a misconfiguration ### Getting 404 on a valid route - Double-check that the `{storeId}` in the URL path exactly matches the store ID shown in the admin panel — it is case-sensitive and must be all lowercase - Verify the endpoint path is correct, including whether it uses `/admin/` for admin routes ### Getting 429 Too Many Requests - Read the `Retry-After` response header for the required wait time in seconds - Implement exponential backoff in your integration rather than retrying at a fixed rate - If rate limiting is a persistent problem, review whether your integration is making redundant or polling-style requests that could be batched or cached # ===== Webhooks ===== (https://docs.nanocart.io/#settings-api) # Webhooks Webhooks let your server receive real-time notifications when things happen in your nanocart store. Instead of polling the API for changes, nanocart pushes data to you the moment an event occurs. ## What Are Webhooks? A webhook is an HTTP POST request that nanocart sends to a URL you specify whenever a store event fires — an order is placed, a product runs low on stock, a checkout is abandoned, and so on. Your server receives the payload, processes it, and returns a response. Common use cases include syncing orders to a CRM or ERP system, triggering custom email or SMS sequences, kicking off fulfillment automation, tracking inventory in external systems, and sending Slack or Discord notifications. Webhooks are configured in **Admin Panel → Settings → Webhooks**. Enter your publicly accessible endpoint URL and save — nanocart generates a webhook secret you'll use to verify incoming requests. ## Request Format Every webhook is an HTTP POST with `Content-Type: application/json`. The body follows this structure: ```json { "eventType": "order.created", "storeId": "your-store-id", "timestamp": "2026-06-17T14:32:00Z", "data": { ... } } ``` | Field | Type | Description | |-------|------|-------------| | `eventType` | string | The event that fired (see Event Types below) | | `storeId` | string | Your store's unique identifier | | `timestamp` | string | ISO 8601 UTC timestamp of when the event was sent | | `data` | object | Event-specific payload (see Event Types below) | ## Verification Headers Every request includes three headers. **Verify all three before processing any webhook.** | Header | Description | |--------|-------------| | `x-nanocart-signature` | `sha256=` followed by the HMAC-SHA256 hex digest of the raw request body, computed using your webhook secret | | `x-nanocart-timestamp` | Unix timestamp (seconds) when the event was sent | | `x-nanocart-delivery` | Unique UUID for this specific delivery — use for idempotency | ## Verifying the Signature Signature verification protects you from forged or replayed requests. The steps are: 1. Capture the **raw request body** before calling `JSON.parse` on it. Body-parser middleware that transforms the buffer will break verification. 2. Check that `x-nanocart-timestamp` is within **300 seconds** of the current time to prevent replay attacks. 3. Compute `HMAC-SHA256` of the raw body using your webhook secret. 4. Prepend `sha256=` to the hex digest and compare to `x-nanocart-signature` using a **timing-safe comparison** to prevent timing attacks. 5. Return `401` if verification fails — do not process the event. ### Node.js Example ```js const crypto = require('crypto'); function verifyWebhook(rawBody, headers, secret) { const sig = headers['x-nanocart-signature']; const ts = headers['x-nanocart-timestamp']; if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false; const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } ``` Call this before touching `req.body`. If it returns `false`, respond with `401` immediately. ## Event Types ### `order.created` Fires when Stripe confirms payment and the order status is set to `paid`. ```json { "orderId": "ord_abc123", "orderNumber": "1042", "email": "customer@example.com", "customerName": "Jane Smith", "items": [ { "productId": "prod_x", "name": "Surf Tee", "variantName": "M / Blue", "price": 3500, "quantity": 2 } ], "subtotal": 7000, "shippingCost": 800, "taxAmount": 560, "discountAmount": 0, "total": 8360, "status": "paid", "createdAt": "2026-06-17T14:32:00Z" } ``` All monetary values are in **cents**. ### `order.shipped` Fires when the store owner updates an order's status to `shipped` via the admin panel or API. ```json { "orderId": "ord_abc123", "orderNumber": "1042", "email": "customer@example.com", "trackingNumber": "1Z999AA10123456784", "status": "shipped" } ``` ### `order.cancelled` Fires when an order status is updated to `cancelled`. ```json { "orderId": "ord_abc123", "orderNumber": "1042", "email": "customer@example.com", "status": "cancelled" } ``` ### `product.low_stock` Fires when a product or variant's inventory drops to 5 or below after a completed order. ```json { "productId": "prod_x", "productName": "Surf Tee", "slug": "surf-tee", "variantId": "var_m_blue", "variantName": "M / Blue", "inventory": 3 } ``` `inventory` is the current count after the triggering order was processed. ### `checkout.abandoned` Fires when a Stripe checkout session expires without payment — approximately 30 minutes after the session was created. ```json { "orderId": "ord_pending_xyz", "email": "customer@example.com", "items": [ { "productId": "prod_x", "name": "Surf Tee", "quantity": 1, "price": 3500 } ], "subtotal": 3500 } ``` Use this to trigger a recovery email sequence. ### `webhook.test` Fires when you click **Send test event** in Admin Panel → Settings → Webhooks. Use it to confirm your endpoint is reachable and that signature verification is working correctly. ```json { "message": "This is a test event from nanocart." } ``` ## Responding to Webhooks Return **HTTP 200** to acknowledge receipt. Any non-2xx response is treated as a failure and nanocart will retry. - Failed deliveries are retried **up to 3 times** with exponential backoff. - Your endpoint must respond within **10 seconds**. If processing takes longer, return 200 immediately and handle the work asynchronously — add it to a queue, publish to a background job, etc. ## Idempotency The same event may be delivered more than once due to network issues or retries. Use the `x-nanocart-delivery` UUID to deduplicate: store the delivery IDs you have already processed and skip any that appear a second time. ## Your Webhook Secret nanocart generates your webhook secret when you save your endpoint URL in **Admin Panel → Settings → Webhooks**. It is shown **once** — copy it to your server's environment variables immediately. If you lose it, you can regenerate it from the admin panel. The old secret is invalidated immediately, so you must update your server's environment variable before the next webhook fires or all incoming requests will fail signature verification. ## Testing Your Webhook Use the **Send test event** button in Admin Panel → Settings → Webhooks to fire a `webhook.test` event to your endpoint. Your handler should return 200 and log the event. Confirm that signature verification passes — if it does, you're wired up correctly. ## Common Issues **Not receiving webhook events** Your endpoint must be publicly accessible on the internet. Localhost and `127.0.0.1` addresses are not reachable by nanocart's servers. Check for firewall rules, security groups, or reverse-proxy configs that might be blocking incoming POST requests. Also confirm your handler returns HTTP 200. **Signature verification failing** The most common cause is reading `req.body` (already parsed JSON) instead of the raw buffer. Capture the raw body before any JSON parsing. Also verify you are using the webhook secret — not your API key — and that no middleware is modifying the body bytes in transit. **Getting duplicate events** Implement idempotency using the `x-nanocart-delivery` UUID. Store each processed delivery ID (in a database or cache) and skip the handler logic if the ID has already been seen. **Webhook handler timing out** Return 200 immediately and offload work to a background queue or async job. nanocart closes the connection after 10 seconds — any processing that might take longer must happen outside the request/response cycle. **How do I test locally?** Use a tunneling service such as ngrok to expose your local server with a public HTTPS URL: ``` ngrok http 3000 ``` Paste the generated `https://` URL into Admin Panel → Settings → Webhooks and use **Send test event** to verify connectivity. **I lost my webhook secret** Regenerate it in Admin Panel → Settings → Webhooks. Update your server's environment variable immediately — the old secret is no longer valid. # ===== Admin Panel Overview ===== (https://docs.nanocart.io/#settings-store) # nanocart Admin Panel Overview The nanocart admin panel is a web application at **https://nanocart.io/admin** that serves as the central hub for managing all aspects of your nanocart store. From here you can manage products, categories, orders, coupons, reports, storefront settings, billing, and store configuration. The interface is built with Alpine.js, so changes update reactively without full page reloads. Support contact: **hello@nanocart.io** --- ## Accessing the Admin Panel 1. Go to **https://nanocart.io/admin** 2. Sign in with your email and password, or click **"Continue with Google"** to use Google OAuth 3. First-time signup requires email verification — check your inbox for a verification code 4. Forgot your password? Click **"Forgot Password"** on the sign-in page; a reset code will be sent to your email --- ## Creating Your First Store After logging in, click **"Create New Store"** and fill in the following: - **Store ID** (required): 3–32 characters, lowercase letters, numbers, and hyphens only (e.g., `my-cool-shop`). This is permanent — it cannot be changed after creation, as it becomes your API path and storefront URL. - **Stripe Publishable Key**: starts with `pk_live_` (live) or `pk_test_` (testing) - **Stripe Secret Key**: starts with `sk_live_` or `sk_test_` - **Stripe Webhook Secret**: see setup instructions below - **Optional**: store name, domain URL (your storefront URL, used for CORS), brand color (hex) ### Setting Up Your Stripe Webhook 1. In your Stripe Dashboard, go to **Developers → Webhooks → Add endpoint** 2. Set the endpoint URL to `https://api.nanocart.io/shop/{storeId}/webhook` (replace `{storeId}` with your actual store ID) 3. Save the endpoint, then copy the **Signing secret** (starts with `whsec_...`) 4. Paste the signing secret into the **Stripe Webhook Secret** field in nanocart Settings --- ## Admin Panel Navigation ### Dashboard Summary of recent orders, total revenue, current product count, and current tier info. Also displays your ready-to-copy **widget embed code snippet** for adding the cart to your website. ### Products Add new products, edit existing products, archive products, and manage variants and options. ### Categories Create categories and subcategories, set parent/child relationships, reorder using `sortOrder`, and set category status. ### Orders View all orders sorted by date. Update order status (paid, shipped, delivered, cancelled), add tracking numbers, and add internal notes. ### Coupons Create discount codes, view usage statistics, and deactivate coupons. ### Reports Revenue dashboard with a date picker, order count, average order value, tax breakdown by state, and top products. ### Storefront Configure your hosted storefront template, colors, hero content, about page, announcement bar, social links, logo, and custom domain (custom domain requires Growth or Scale hosted plan). ### Settings - Store info: name, domain, brand color, currency - Stripe keys and webhook secret - Tax configuration - Shipping configuration - Email configuration: from name and reply-to email - Order prefix settings - API key: display and regeneration - Webhook URL configuration - Allowed domains list ### Billing View your current plan, upgrade or downgrade, cancel your subscription, view billing history and invoices, and reactivate a plan if cancellation is already scheduled. --- ## Switching Between Multiple Stores If you have more than one store, use the **store switcher dropdown** at the top of the sidebar navigation. Each store has completely independent products, orders, settings, API key, and subscription. Billing is per store. --- ## Your API Key Your API key is visible on the **Settings** page. It starts with `sc_live_` followed by 64 hex characters. Use it in the `x-api-key` header for admin API calls. You can regenerate it at any time — the old key is invalidated immediately. Never share this key. --- ## Cancellation Status Bar A status bar appears at the top of all admin pages when your subscription is scheduled for cancellation (`cancel_at_period_end` is true). It shows your plan name, the expiry date, and a **"Reactivate Plan"** link to undo the cancellation before it takes effect. --- ## Common Actions — Where to Find Them | Task | Location | |---|---| | Add a product | Products → click **+ Add Product** | | View orders | Orders page | | Upgrade your plan | Billing → **Upgrade Plan** button | | Update Stripe keys | Settings → Stripe Configuration section | | Get embed code for your website | Dashboard → embed snippet | | Reset your password | Sign-in page → **Forgot Password** link | | Set up shipping | Settings → Shipping Configuration section | | Set up tax | Settings → Tax Configuration section | | Configure hosted storefront | Storefront page (requires active hosted plan) | | Regenerate API key | Settings → **Regenerate API Key** | --- ## Security Best Practices - Never share your API key (`sc_live_...`) with anyone - Never share your Stripe secret key (`sk_live_...`) with anyone - Use **Google OAuth** for stronger authentication - Regenerate your API key immediately if you suspect it has been compromised (Settings → Regenerate API Key) - Contact **hello@nanocart.io** immediately if you suspect unauthorized access to your account --- ## Common Questions **Can I have multiple team members / users on one account?** Not currently. Each account supports one login. Contact hello@nanocart.io if multi-user access is a need. **Can I have multiple stores?** Yes. Create additional stores from the admin panel using the "Create New Store" option. Each store is billed separately and has its own independent settings and API key. **I can't log in — what do I do?** 1. Try **Forgot Password** first to reset your credentials 2. If the reset email doesn't arrive, check your spam folder 3. If you are still locked out, contact **hello@nanocart.io** **My Stripe webhook isn't working.** Double-check that the endpoint URL in your Stripe Dashboard matches `https://api.nanocart.io/shop/{storeId}/webhook` exactly, and that the webhook signing secret in nanocart Settings matches the one shown in Stripe. See the Stripe webhook setup steps above. **Where do I find my embed code?** On the **Dashboard** page — the widget embed code snippet is displayed prominently and is ready to copy and paste into your website's HTML. # ===== Billing And Subscriptions ===== (https://docs.nanocart.io/#billing-overview) # Billing & Subscriptions This document covers how nanocart billing works, how to manage your subscription, and answers to common billing questions. --- ## How nanocart Billing Works nanocart uses **Stripe** to handle platform billing — that is, billing *you* for your nanocart subscription tier. This is completely separate from your store's own Stripe account, which is used to collect payments from your customers. - **Free plan**: No credit card required. You will never be charged on the Free plan. - **Paid plans**: A Stripe subscription is created and billed to your credit card on a monthly or annual basis. - Your store's Stripe account (customer payments) and nanocart's platform billing are two entirely independent Stripe connections. A charge on one has no effect on the other. --- ## Plans and Pricing ### Widget Plans | Plan | Monthly | Annual | Products | |------|---------|--------|----------| | Free | $0 | $0 | Up to 2 | | Standard | $5/mo | $50/yr | Up to 25 | | Pro | $10/mo | $100/yr | Up to 100 | | Expert | $25/mo | $250/yr | Unlimited | Annual billing saves you the equivalent of 2 months compared to paying monthly. ### Hosted Storefront Plans (separate add-on subscription) | Plan | Monthly | |------|---------| | Starter | $7/mo | | Standard | $15/mo | | Growth | $25/mo | | Scale | $49/mo | Hosted storefront plans are an optional add-on and billed as a separate Stripe subscription from your widget plan. --- ## Subscribing for the First Time (Free to Paid) 1. Go to **Admin Panel → Billing**. 2. Choose your desired plan and billing period. 3. You will be redirected to **Stripe Checkout** to enter your card details. 4. After successful payment, you are redirected back to the admin panel and your new tier is **immediately activated**. --- ## Upgrading an Existing Subscription Upgrades (for example, Standard → Pro) are handled **entirely within the admin panel** — no redirect to Stripe Checkout. - nanocart modifies your existing Stripe subscription in place using proration. - You are **immediately billed** the prorated difference for the remaining days in your current billing cycle. - The prorated invoice appears right away in your billing history. **Example:** You are 10 days into a $5/month Standard plan and upgrade to Pro at $10/month. With 20 days remaining in the billing period, you are charged approximately $3.33 (20 days × $0.50/day difference). Your next full renewal is billed at the new Pro rate. --- ## Switching Between Monthly and Annual Billing Switching billing periods (monthly → annual or annual → monthly) uses the same proration mechanism as upgrading. - **Monthly to annual**: The full annual amount is charged immediately, minus a credit for the unused days remaining on your current monthly cycle. - **Annual to monthly**: The change takes effect at the start of your next billing cycle. To switch, go to **Admin Panel → Billing** and select your preferred billing period. --- ## Canceling Your Subscription 1. Go to **Admin Panel → Billing** and click **Cancel Plan**. 2. Cancellation is scheduled for the end of your current billing period (`cancel_at_period_end = true`). 3. You **keep full access** to your current tier until the billing period ends. 4. No refunds are issued for partial months. 5. At period end, your tier automatically reverts to Free and the subscription is removed. Your cancellation date and remaining access period are shown on the Billing page. --- ## Reactivating a Scheduled Cancellation If you have scheduled a cancellation but the billing period has not yet ended, you can undo it: 1. Go to **Admin Panel → Billing** (or click **Reactivate Plan** in the status bar at the top of the admin). 2. The cancellation is immediately reversed — **no charge is made** and your renewal date is unchanged. 3. A **Reinstated** event (blue badge) is recorded in your billing history. --- ## Billing History The Billing page in the admin panel shows a full history of your subscription activity. Each entry includes: - **Date** of the charge or event - **Description** (plan name and billing period) - **Amount** - **Status badge**: - **Paid** (green) — successful invoice payment - **Cancelled** (grey) — subscription cancelled - **Reinstated** (blue) — cancellation was reversed Paid invoices are clickable and open a PDF from Stripe for your records. $0 events (such as scheduling a cancellation or reactivating a plan) are also recorded in billing history as line items. These do not have an invoice PDF but are visible in the list. --- ## Common Billing Questions **I was charged twice this month — why?** If you upgraded your plan mid-cycle, a prorated invoice is generated immediately at the time of upgrade. This is a separate charge from your regular monthly invoice and is not a duplicate. The prorated amount includes a credit for unused days on your previous plan. **I cancelled my plan but I still have access to all my features.** This is expected behavior. When you cancel, your access continues until the end of the current billing period. You can see the exact end date on the Billing page. At that point, your tier automatically reverts to Free. **My renewal payment failed. What happens?** Stripe will automatically retry the payment over several days. Check that your payment method on file is valid and up to date. If retries are exhausted, Stripe cancels the subscription and your tier reverts to Free. To restore access, re-subscribe from Admin Panel → Billing. **Can I get a refund?** nanocart does not offer refunds for partial months or unused time. For exceptional circumstances, contact [hello@nanocart.io](mailto:hello@nanocart.io). **How do I change my billing email or payment method?** Payment method and billing email are managed through your Stripe customer portal. Contact [hello@nanocart.io](mailto:hello@nanocart.io) to receive a direct link to your portal. **Can I switch from monthly to annual billing?** Yes. Go to Admin Panel → Billing and select annual billing. The prorated difference is charged immediately, giving you credit for unused days on your current monthly cycle. **What happens to my store if my subscription lapses?** Your store and all data remain intact. nanocart only enforces tier limits (product count, shipping methods, etc.). If you exceed Free plan limits after downgrade, existing data is preserved but you may need to reduce your active product count or upgrade to restore full functionality. --- ## Subscription Status Reference When reviewing your subscription via the admin panel or API, you may encounter these fields: | Field | Values | Meaning | |-------|--------|---------| | `tier` | `free`, `standard`, `pro`, `expert` | Your current plan | | `subscriptionStatus` | `active`, `past_due`, `canceled` | Stripe subscription state | | `billingPeriod` | `monthly`, `annual` | Your current billing cycle | | `cancelAtPeriodEnd` | `true` / `false` | Whether cancellation is scheduled | | `renewalDate` | ISO date string | Next billing date (or cancellation date if scheduled) | --- ## Contact Support For billing issues not resolved by this document, contact **[hello@nanocart.io](mailto:hello@nanocart.io)**. Please include your store ID and a description of the issue. For payment disputes or Stripe-specific questions, reference your invoice PDF (available in Billing History) when reaching out. # ===== Troubleshooting And FAQ ===== (https://docs.nanocart.io/#getting-started-intro) # nanocart Troubleshooting & FAQ This document is a comprehensive reference for diagnosing and resolving common nanocart issues. It is written for support agents and AI assistants answering customer questions. Each entry follows a **Problem → Cause(s) → Solution(s)** structure. --- ## Widget Issues ### Q: The widget is not loading / no cart icon is visible on my site **Causes:** - The `data-store-id` attribute is missing from the script tag, or the value does not exactly match the storeId shown in the admin panel (the match is case-sensitive). - The `