The widget is fully themeable through a stable, documented API — script-tag attributes for the common knobs, and CSS custom properties on the host element (id nanocart-widget) for complete control. Merchants can also theme with zero code from the admin portal (Settings → Widget Appearance).
/* Full control: set --nc-* variables on the stable host id */
#nanocart-widget {
--nc-accent: #EF3E32;
--nc-bg: #1d2230;
--nc-radius: 12px;
}
Your page CSS always wins over portal settings and attributes. The complete variable list (--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) with defaults is documented in the support docs theming guide.
Product Alerts Signup (Pro/Expert)
Capture emails for product updates with the <nanocart-signup> element — bare email field + button in your store color; add show-options to render the option checkboxes configured in the portal (Subscribers page). Renders nothing unless the store is Pro/Expert with Product Alerts enabled. Style via nanocart-signup::part(row|input|button|option|consent|message); override text with the label, placeholder, success-message, and error-message attributes.
HTML
<nanocart-signup show-options></nanocart-signup>
Public endpoints (no auth): GET /shop/{storeId}/alerts-config returns the form config (options, consent, accent) or {"enabled": false}; POST /shop/{storeId}/alerts-signup accepts {email, options[], consent}. Bot protection is invisible (honeypot + timing + rate limits) — no captcha needed.
JavaScript API
The widget exposes window.nanocart for programmatic control:
The element renders inside a Shadow DOM, so its styles are fully isolated from your page CSS. It displays the product image, name, price, variant selector (including multi-axis variants), and an add-to-cart button — all synced with your store's live data.
Attributes
Attribute
Required
Description
slug
required
Product slug. Must match a slug returned by the products endpoint.
Multi-axis variant support
When a product has multiple option axes (e.g. Size and Color), the component renders a separate selector for each axis. The active variant is resolved from the combination of all selected values using the optionValues map on each variant (see Get Product).
HTML
<!-- Single product card -->
<nanocart-product slug="trucker-surf-hat"></nanocart-product>
<!-- Multiple cards side by side -->
<div style="display:flex;gap:16px">
<nanocart-product slug="trucker-surf-hat"></nanocart-product>
<nanocart-product slug="board-shorts"></nanocart-product>
</div>
Authentication
Public endpoints (products, categories, checkout) require no authentication. They're meant to be called from your storefront.
Admin endpoints require an API key passed in the x-api-key header. You can find your API key in the admin panel under Settings.
API Key Format:sc_live_ followed by 64 hex characters. Keep your API key secret — it provides full admin access to your store.
Conventions
Prices
All monetary values are in cents (integers). Divide by 100 for display.
API Value
Display
999
$9.99
2800
$28.00
0
$0.00 (free)
Multi-currency: Each store has a single currency field (e.g. usd, eur, gbp). All price values in the API are in that currency's minor unit (cents for USD, pence for GBP, etc.). The currency is returned in the storefront config and store object so you can format amounts correctly for your locale.
Pagination
List endpoints return a lastKey field. If non-null, pass it as a query parameter to get the next page:
GET /shop/my-store/products?limit=20&lastKey=eyJzdG9yZUlkIjoi...
Dates
All timestamps are ISO 8601 UTC strings: 2026-02-24T15:30:00Z
Response Format
All responses return JSON with appropriate HTTP status codes. Error responses include an error field and optional code field.
Service Tiers
Each store has a tier that determines limits. Upgrade from the admin panel or via the subscription API.
Limit
Free
Standard
Pro
Expert
Price
$0
$5/mo
$10/mo
$25/mo
Active Products
2
25
100
Unlimited
Monthly Orders
5
500
5,000
Unlimited
Coupons
1
5
10
Unlimited
File Upload
5 MB
25 MB
50 MB
500 MB
Shipping Methods
Flat rate, Free
All
All
All
Local Pickup
No
Yes
Yes
Yes
Public API
These endpoints require no authentication. Call them from your storefront to display products and process orders.
GET/shop/{storeId}/products
List active products with optional filtering, sorting, and pagination.
List all active categories for a store, sorted by sortOrder. Categories support a parent/child hierarchy via the parentId field. Top-level categories have an empty parentId. Subcategories reference their parent's categoryId.
Empty string for top-level categories. Set to a parent categoryId to make this a subcategory.
sortOrder
number
Display order (lower = first)
status
string
active or hidden
Subcategory pattern: To build a hierarchical category tree, filter categories where parentId is empty to get top-level parents, then filter where parentId === parent.categoryId to get children. Products are assigned to a single categoryId — when displaying a parent category, you may want to also include products from its subcategories.
POST/shop/{storeId}/coupons/validate
Validate a coupon code and calculate the discount amount.
Returns which payment processors are configured for the store. Use this to decide which checkout buttons to show — only display buttons for processors that are actually enabled.
Response
JSON
{
"stripe": true,
"paypal": false
}
Both fields are booleans. A value of false means that processor is not configured for the store — do not show its checkout button. If both are false, show a "payments not configured" message rather than a dead button.
The nanocart widget calls this endpoint automatically on first cart open and caches the result for the page session. If you are building a custom checkout UI, fetch this endpoint once and render buttons accordingly.
POST/shop/{storeId}/checkout
Create a checkout session. Validates inventory, applies coupons, calculates shipping and tax, and returns a payment URL to redirect the customer to. Supports Stripe and PayPal.
Redirect the customer to sessionUrl to complete payment. For Stripe this is a checkout.stripe.com URL; for PayPal it is a paypal.com approval URL. The redirect and return flow is identical from your code's perspective regardless of processor.
Errors
Status
Error
400
Cart is empty
400
Email address is required for checkout.
400
Product {id} is not available
400
{item} is out of stock
400
PayPal is not configured for this store
403
This store has reached its monthly order limit
Redirect URLs: After payment, the customer is redirected back to your site using the Referer or Origin header. Store the orderId in localStorage before redirecting so you can look up the order on your success page. This works the same for both Stripe and PayPal.
Get the storefront configuration for a hosted store. Used by the storefront SPA on page load to get template, branding, and content settings. Only works for stores with an active hosted plan.
Look up a custom domain to find the associated store. Used by the hosted storefront SPA when running on a custom domain instead of a *.nanocart.io subdomain.
Query Parameters
Parameter
Type
Required
Description
domain
string
required
The custom domain to resolve (e.g. shop.mybrand.com)
Response
JSON
{
"storeId": "my-store"
}
Errors
Status
Response
400
{"error": "domain parameter is required"}
404
{"error": "Domain not found"}
GET/shop/{storeId}/donation-config/{campaignId}
Configuration for the donate popup: amounts, frequencies, button text, branding. _default as the campaignId resolves the store's oldest active campaign. Inactive or missing campaigns return {"enabled": false}. Cached 5 minutes.
Live campaign stats for the <nanocart-donate-stats> widget. Only fields the merchant enabled in the campaign's stats settings are present — a disabled field is absent, not zero, so a "count only" widget receives no money figures at all. Cached 5 minutes.
Create a donation checkout session and get the redirect URL. The amount, frequency, currency, and campaign relationship are validated server-side — client values are never trusted for money math.
Redirect the donor to sessionUrl. Donation records are finalized by webhooks — never rely on the browser's return. Error codes: CAMPAIGN_NOT_FOUND, AMOUNT_NOT_ALLOWED, FREQUENCY_NOT_ALLOWED, PROCESSOR_NOT_SUPPORTED, STRIPE_NOT_CONFIGURED, PAYPAL_NOT_CONFIGURED.
Admin API
All admin endpoints require authentication via the x-api-key header.
Product variants (variantId, name, price, sku, inventory, optionValues). optionValues is a key/value map of option axis name → selected value (e.g. {"Color":"Navy","Size":"Small"}). Used by the widget and web component for multi-axis variant resolution. Legacy flat-name format still supported.
options
array
optional
Option definitions for variant generation
productType
string
optional
physical (default) or digital
status
string
optional
draft (default), active
featured
boolean
optional
Featured product flag
slug
string
optional
URL slug. Auto-generated from name if omitted.
taxable
boolean
optional
Subject to tax (default true)
tags
array
optional
String tags for organization
shippingCost
integer
optional
Per-item shipping cost in cents (for per_item method)
fulfillmentVendorId
string
optional
Route this product to a fulfillment vendor — its orders are emailed to the vendor automatically (Pro/Expert)
vendorSku
string
optional
The vendor's own item number, shown on vendor order sheets. Variants may carry their own vendorSku that overrides this.
vendorNotes
string
optional
Per-product instruction for the vendor (e.g. "Front print, design #12")
{
"settingKey": "shipping_config",
"value": {
"method": "flat_rate",
"flatRate": 599,
"freeShippingEnabled": true,
"freeShippingThreshold": 7500,
"localPickupEnabled": true,
"localPickupInstructions": "Pick up at 123 Main St, 9AM-5PM"
}
}
Setting Key
Description
tax_config
Sales tax: enabled (master toggle), useStripeTax (automatic Stripe Tax for Stripe checkouts), defaultRate (flat percent applied to PayPal orders, and to Stripe orders when Stripe Tax is off)
shipping_config
Shipping method, rates, free shipping, local pickup
email_config
From email address and name for order emails
order_config
Order number prefix and next number
Shipping Methods:flat_rate (single rate), per_item (per product), tiered (by subtotal), free. Free tier stores are limited to flat_rate and free.
Reports
GET/shop/{storeId}/admin/reports
Get sales reports with revenue, order counts, top products, and tax breakdown.
Upload your file with a PUT request to uploadUrl with the Content-Type header matching what you specified. Then use fileUrl in your product images array, category image field, etc.
Storefront
GET/shop/{storeId}/admin/storefront
Get the current storefront settings for your store.
Reactivate a subscription that was scheduled for cancellation. Undoes a cancel without charging anything — the next renewal date is unchanged.
Response
JSON
{
"message": "Your Standard Widget plan has been reactivated. Your next renewal is unchanged."
}
Errors
Status
Response
400
{"error": "No subscription found."}
400
{"error": "Your subscription is not scheduled for cancellation."}
Webhooks
Webhooks let nanocart push real-time events to your server when things happen in your store. Configure a webhook URL in the admin panel under Settings → Webhooks.
Verification
Every request includes three headers you should verify before processing:
Header
Description
x-nanocart-signature
sha256=<HMAC-SHA256 hex digest> — computed over the raw request body using your webhook secret
x-nanocart-timestamp
Unix timestamp (seconds) of delivery. Reject if >300s old to prevent replay attacks.
Sent when you click "Send test event" in the admin panel. Use it to confirm your endpoint is reachable and your signature verification is correct.
JSON
{
"eventType": "webhook.test",
"storeId": "my-store",
"timestamp": "2026-06-17T14:00:00Z",
"data": {
"message": "This is a test event from nanocart."
}
}
Subscribers
Email capture, broadcasts, and custom email templates. Requires a Pro or Expert plan; other tiers receive 403 with "code": "TIER_LIMIT". Subscribers sign up through the <nanocart-signup> widget element or your hosted storefront; every broadcast automatically carries a signed unsubscribe link, and unsubscribed/bounced/complained addresses are suppressed for you.
Remove a subscriber. URL-encode the email address in the path.
POST/shop/{storeId}/admin/subscribers/send
Send a broadcast email to your subscribers. Runs as an async job — poll the sends history for status.
Request Body
JSON
{
"kind": "product",
"productId": "dd9392ed-...",
"subject": "New drop just landed",
"message": "Fresh colors are live — grab yours.",
"productLink": "https://mystore.com/shop",
"templateId": "tpl-123",
"options": ["New drops"],
"testOnly": false
}
Field
Type
Required
Description
kind
string
required
product (announcement with a product card) or general
productId
string
optional
Required when kind = product
subject
string
optional
Max 150 chars. Falls back to the template's default subject.
message
string
optional
Max 5000 chars. Optional when the chosen template has no {{message}} slot.
productLink
string
optional
URL for the "View product" button
templateId
string
optional
Custom template to use. Omit for the standard branded layout.
options
array
optional
Restrict the audience to subscribers who picked these alert options
testOnly
boolean
optional
true sends a single preview to your store contact email only
Response (202)
JSON
{ "sendId": "b1afb5d6-..." }
Audience is capped at 2,000 recipients per send. Suppressed addresses (unsubscribed, bounced, complained) are skipped automatically and reported in the send detail.
GET/shop/{storeId}/admin/subscribers/sends
Send history (latest 20). Status is pending while the job runs, then completed or failed with counts.
detail is null for sends made before per-recipient recording was introduced (July 2026). Details are retained for 90 days.
GET/shop/{storeId}/admin/subscribers/templates
List custom email templates (metadata only — fetch one for its HTML). Up to 20 per store.
POST/shop/{storeId}/admin/subscribers/templates
Create a custom email template.
Request Body
Field
Type
Required
Description
name
string
required
Max 60 chars
subject
string
optional
Default subject for sends using this template (max 150)
html
string
required
Email body HTML, max 50 KB. Placeholders: {{store.name}}, {{store.logo}}, {{message}}, {{product.name}}, {{product.price}}, {{product.image}}, {{product.url}}, {{product.description}}, {{product_card}}, {{unsubscribe_url}}. Unknown tags are stripped; if you omit the unsubscribe link, a standard compliance footer is appended automatically.
Donation campaigns, records, supporters, and reporting. Available on every plan: Free/Standard run 1 campaign, Pro/Expert up to 10 and may hide the "Powered by NanoCart" branding. Amounts are integer cents throughout. Monthly donations additionally require the merchant's Stripe webhook to include invoice.paid, invoice.payment_failed, customer.subscription.deleted, and customer.subscription.updated.
GET/shop/{storeId}/admin/donations/campaigns
List campaigns with live totals. Response includes limit (your plan's campaign cap) and brandingRemovable.
POST/shop/{storeId}/admin/donations/campaigns
Create a campaign. campaignId is the public slug used by the donate button and stats widget — lowercase letters, numbers, dashes; immutable after creation.
Up to 8 amounts each, 100–999999 cents. Monthly requires at least one when allowRecurring is true.
allowCustomAmount
boolean
optional
One-time "other amount" field (default true; custom recurring is not supported)
goalAmountCents / goalMetric
integer / string
optional
Goal measured by month_total, mrr, or all_time — recurring revenue and totals are never mixed
statsConfig
object
optional
What the PUBLIC stats widget may show: showCount, showRaised, showMonthly, showGoal, showButton. Disabled fields never leave the API.
hideBranding
boolean
optional
Pro/Expert only — otherwise 403 TIER_LIMIT
Response is 201 with the campaign incl. zeroed counters. A store at its campaign cap receives 403 TIER_LIMIT; a duplicate slug receives 409 DUPLICATE_CAMPAIGN.
Update a campaign. Full replace — GET first, modify, and PUT the complete config (live counters and createdAt are preserved server-side; the slug cannot change). Same body and validation as create.
Delete a campaign — the donate button and stats widget stop rendering immediately. Donation records are kept; active Stripe subscriptions are NOT canceled (do that in Stripe).
GET/shop/{storeId}/admin/donations
List donation records, newest first.
Query Parameters
Parameter
Type
Description
kind
string
one_time, recurring (supporter records), or recurring_payment (individual monthly charges)
Custom fulfillment partners — a local print shop, a drop-ship supplier — that automatically receive an emailed order sheet when their products sell. Requires a Pro or Expert plan; other tiers receive 403 with "code": "TIER_LIMIT". Up to 10 vendors per store.
No prices, ever. Vendor emails are work orders, not receipts. The template system has no price placeholders, so a vendor email cannot include what you charge — by construction. Route products to a vendor with the fulfillmentVendorId, vendorSku, and vendorNotes fields on products (variants may carry their own vendorSku).
GET/shop/{storeId}/admin/vendors
List vendors (metadata only — fetch one for its templateHtml).
active (default) or paused. Paused vendors are skipped at order time (recorded as skipped on the order).
subjectTemplate
string
optional
Subject line; tags {{order.number}}, {{store.name}}, {{vendor.name}} resolve. Max 150.
templateHtml
string
optional
Custom order-sheet HTML, max 50 KB. Empty = the standard NanoCart order sheet. Custom HTML must include {{items_table}} or an {{#items}}…{{/items}} block, or the request fails with NO_ITEMS_PLACEHOLDER. See the tag table below.
includeShipping
boolean
optional
Fill {{shipping_address}} with the customer ship-to (default true)
includeCustomerContact
boolean
optional
Fill {{customer.name}}/{{customer.email}} (default false)
notes
string
optional
Standing instructions sent with every order via {{vendor.notes}} (max 2000)
Unknown tags are stripped at send time. There are deliberately no price tags. Response is 201 with the full vendor object; a store at the 10-vendor cap receives 409 with "code": "LIMIT_REACHED".
GET/shop/{storeId}/admin/vendors/{vendorId}
Fetch one vendor including its templateHtml.
PUT/shop/{storeId}/admin/vendors/{vendorId}
Update a vendor. Full replace — GET the vendor first, modify, and PUT the complete record. A partial body resets omitted fields (an update without templateHtml clears the saved template). Same body and validation as create.
DELETE/shop/{storeId}/admin/vendors/{vendorId}
Delete a vendor. Products keep selling; their orders simply stop being forwarded. Order history is retained.
POST/shop/{storeId}/admin/vendors/draft
AI-draft (or revise) order-sheet HTML from a description. Free. Body: description (required, ≤1000 chars), currentHtml (optional). Returns { "html": "..." }.
POST/shop/{storeId}/admin/vendors/{vendorId}/test
Send yourself a test order sheet rendered with sample data. Goes to your store contact email with a [TEST] banner — the vendor is never contacted. Returns { "sentTo": "you@example.com" }.
Manually email an order to a vendor — for orders placed before the vendor was configured (items are matched by the products' current vendor routing) or deliberate re-sends.
Request Body
JSON
{ "vendorId": "5d195554-..." }
Orders containing vendor-routed items are emailed automatically after payment — this endpoint is only for manual sends. Failed automatic sends can be retried with POST /admin/orders/{orderId}/retry-pod using {"provider": "vendor"}; outcomes are recorded on the order's podFulfillments array with "provider": "vendor". While Test Mode (Settings → Fulfillment) is on, vendor emails are redirected to your store contact email with a [TEST] banner.
Error Codes
Error responses include an error message and an optional code for programmatic handling:
JSON
{
"error": "Free plan allows 2 active products. Upgrade to Standard for 25.",
"code": "TIER_PRODUCT_LIMIT",
"tierLimit": true
}
Code
HTTP
Description
INVALID_API_KEY
401
API key doesn't exist or is invalid
STORE_SUSPENDED
403
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 wrong
STRIPE_AUTH_ERROR
400
Stripe rejected the keys
DOMAIN_NOT_ALLOWED
403
Origin domain not in allowedDomains
TIER_PRODUCT_LIMIT
403
Exceeded active product limit for tier
TIER_ORDER_LIMIT
403
Exceeded monthly order limit for tier
TIER_COUPON_LIMIT
403
Exceeded coupon limit for tier
TIER_UPLOAD_LIMIT
403
File size exceeds tier upload limit
TIER_SHIPPING_RESTRICTED
403
Shipping method not available on tier
DUPLICATE_COUPON
409
Coupon code already exists
Tier limit errors include "tierLimit": true so your app can detect upgrade prompts. The error message includes the current limit and the next tier's limit.