API Documentation
Everything you need to integrate nanocart into your site.
Widget Integration
Add the nanocart widget to any page with a single script tag. No build step, no dependencies.
Script Tag
Include this once per page, replacing your-store with your store ID:
<script src="https://cdn.nanocart.io/v1/nanocart.min.js" data-store-id="your-store"></script>
Button Types
Use data attributes to create interactive buttons. All you need is the product slug from your admin panel.
| Attribute | Behavior | Example |
|---|---|---|
| data-nanocart-buy="slug" | Adds product to cart and opens the cart drawer. Best for "Buy Now" actions. | <button data-nanocart-buy="trucker-hat">Buy Now</button> |
| data-nanocart-add="slug" | Adds product to cart and opens the cart drawer. Same as buy, use whichever label fits. | <button data-nanocart-add="trucker-hat">Add to Cart</button> |
| data-nanocart-product="slug" | Opens a product detail modal with image, variant selector, and add-to-cart. | <button data-nanocart-product="trucker-hat">View Details</button> |
Full Example
<!DOCTYPE html>
<html>
<body>
<h1>My Store</h1>
<button data-nanocart-buy="trucker-hat">Buy Now</button>
<button data-nanocart-product="trucker-hat">View Details</button>
<script src="https://cdn.nanocart.io/v1/nanocart.min.js"
data-store-id="your-store"></script>
</body>
</html>
Theming
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).
<!-- Attributes: light/dark base + accent --> <script src="https://cdn.nanocart.io/v1/nanocart.min.js" data-store-id="your-store" data-theme="light" data-accent-color="#EF3E32"></script>
/* Full control: set --nc-* variables on the stable host id */
#nanocart-widget {
--nc-accent: #EF3E32;
--nc-bg: #1d2230;
--nc-radius: 12px;
}
--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.
<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:
// Add product by slug
nanocart.addBySlug('trucker-hat');
// Open product detail modal
nanocart.viewProduct('trucker-hat');
// Cart controls
nanocart.open();
nanocart.close();
nanocart.toggle();
nanocart.getItems();
nanocart.getCount();
nanocart.getSubtotal();
nanocart.clear();
Web Component: <nanocart-product>
Embed a fully self-contained product card anywhere on your page using the custom element. No JavaScript required beyond the widget script.
<nanocart-product slug="trucker-surf-hat"></nanocart-product>
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).
<!-- 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.
curl https://api.nanocart.io/shop/my-store/admin/products \ -H "x-api-key: sc_live_your_api_key_here"
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) |
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.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| category | string | — | Filter by category slug |
| sort | string | newest | newest, price, or featured |
| limit | integer | 50 | Max results per page (max 50) |
| lastKey | string | — | Pagination token from previous response |
Response
{
"products": [
{
"productId": "dd9392ed-ea5a-494c-adbc-a8ce5288c2e8",
"slug": "trucker-surf-hat",
"name": "Trucker Surf Hat",
"price": 2800,
"compareAtPrice": null,
"images": ["https://images.unsplash.com/photo-..."],
"variants": [
{
"variantId": "d90da05a",
"name": "Adjustable / Navy",
"price": 2800,
"sku": "TSH-ADJ-NVY",
"optionValues": { "Size": "Adjustable", "Color": "Navy" }
}
],
"featured": false,
"categoryId": "abc-123",
"inventory": 50,
"productType": "physical",
"status": "active",
"createdAt": "2026-02-23T20:59:28Z",
"updatedAt": "2026-02-23T20:59:28Z"
}
],
"lastKey": null
}
Response
{
"product": {
"productId": "dd9392ed-ea5a-494c-adbc-a8ce5288c2e8",
"slug": "trucker-surf-hat",
"name": "Trucker Surf Hat",
"description": "Classic trucker hat with embroidered surf logo.",
"price": 2800,
"compareAtPrice": null,
"categoryId": "abc-123",
"images": ["https://images.unsplash.com/photo-..."],
"variants": [
{
"variantId": "d90da05a",
"name": "Adjustable / Navy",
"price": 2800,
"sku": "TSH-ADJ-NVY",
"inventory": 25,
"optionValues": { "Size": "Adjustable", "Color": "Navy" }
}
],
"options": [
{ "name": "Size", "values": ["Adjustable"] },
{ "name": "Color", "values": ["Navy", "Black"] }
],
"inventory": 50,
"productType": "physical",
"shippingRequired": true,
"taxable": true,
"tags": ["hats", "surf"],
"featured": false,
"status": "active",
"createdAt": "2026-02-23T20:59:28Z",
"updatedAt": "2026-02-23T20:59:28Z"
}
}
Errors
| Status | Response |
|---|---|
| 404 | {"error": "Product not found"} |
parentId field. Top-level categories have an empty parentId. Subcategories reference their parent's categoryId.Response
{
"categories": [
{
"categoryId": "cat-001",
"name": "Men's",
"slug": "mens",
"description": "Men's clothing and accessories",
"image": "https://...",
"parentId": "",
"sortOrder": 0,
"status": "active"
},
{
"categoryId": "cat-002",
"name": "Shirts",
"slug": "shirts",
"description": "Men's shirts",
"image": "https://...",
"parentId": "cat-001",
"sortOrder": 0,
"status": "active"
},
{
"categoryId": "cat-003",
"name": "Pants",
"slug": "pants",
"description": "",
"image": "",
"parentId": "cat-001",
"sortOrder": 1,
"status": "active"
}
]
}
Category Fields
| Field | Type | Description |
|---|---|---|
| categoryId | string | Unique category identifier |
| name | string | Category display name |
| slug | string | URL-friendly name (unique within store) |
| description | string | Optional description |
| image | string | Optional image URL (for category cards) |
| parentId | string | 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 |
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.
Request Body
{
"code": "SAVE20",
"subtotal": 4999
}
Response (valid)
{
"valid": true,
"type": "percent_off",
"value": 20,
"discountAmount": 999,
"message": "20% off applied!"
}
Response (invalid)
{
"valid": false,
"message": "This coupon has expired"
}
percent_off, fixed_amount, free_shippingResponse
{
"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.
Request Body
{
"items": [
{
"productId": "dd9392ed-ea5a-494c-adbc-a8ce5288c2e8",
"variantId": "d90da05a",
"quantity": 2
}
],
"email": "customer@example.com",
"couponCode": "SAVE20",
"shippingMethod": "standard",
"processor": "stripe"
}
| Field | Type | Required | Description |
|---|---|---|---|
| items | array | required | Cart items with productId, optional variantId, and quantity |
| string | required | Customer email for order confirmation | |
| couponCode | string | optional | Coupon code to apply |
| shippingMethod | string | optional | standard (default) or local_pickup |
| processor | string | optional | stripe (default) or paypal — must match a processor configured for the store |
Response
{
"sessionUrl": "https://www.sandbox.paypal.com/checkoutnow?token=...",
"orderId": "a1b2c3d4-e5f6-..."
}
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 |
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.Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | required | Must match the order's email |
Response
{
"order": {
"orderId": "a1b2c3d4-e5f6-...",
"orderNumber": "MYS-1001",
"email": "customer@example.com",
"customerName": "John Doe",
"items": [
{
"productId": "dd9392ed-...",
"name": "Trucker Surf Hat",
"variantName": "Adjustable / Navy",
"price": 2800,
"quantity": 2
}
],
"subtotal": 5600,
"shippingCost": 499,
"shippingMethod": "standard",
"taxAmount": 336,
"discountAmount": 0,
"total": 6435,
"shippingAddress": {
"name": "John Doe",
"line1": "123 Main St",
"city": "Charleston",
"state": "SC",
"zip": "29401",
"country": "US"
},
"status": "paid",
"createdAt": "2026-02-24T12:00:00Z"
}
}
Response
{
"storeId": "my-store",
"storeName": "My Store",
"currency": "usd",
"template": "classic",
"accentColor": "#418a9e",
"backgroundColor": "#FFFFFF",
"textColor": "#333333",
"storeDescription": "Your store description here",
"aboutContent": "About page content...",
"aboutTitle": "About My Store",
"aboutImage": "https://...",
"heroTitle": "Welcome to My Store",
"heroLayout": "center",
"heroImage": "",
"logo": "https://...",
"showStoreName": true,
"favicon": "",
"announcementBar": {
"enabled": false,
"text": "",
"backgroundColor": "#418a9e",
"textColor": "#FFFFFF"
},
"socialLinks": {
"instagram": "",
"twitter": "",
"facebook": "",
"tiktok": "",
"youtube": ""
},
"showPoweredBy": true
}
*.nanocart.io subdomain.Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| domain | string | required | The custom domain to resolve (e.g. shop.mybrand.com) |
Response
{
"storeId": "my-store"
}
Errors
| Status | Response |
|---|---|
| 400 | {"error": "domain parameter is required"} |
| 404 | {"error": "Domain not found"} |
Admin API
All admin endpoints require authentication via the x-api-key header.
Stores
Response
{
"stores": [
{
"storeId": "my-store",
"name": "My Store",
"tier": "standard",
"status": "active",
"domain": "https://mystore.com",
"contactEmail": "me@mystore.com",
"currency": "usd",
"brandColor": "#418a9e",
"apiKey": "sc_live_...",
"createdAt": "2026-02-20T10:00:00Z"
}
]
}
Request Body
{
"storeId": "my-store",
"name": "My Store",
"domain": "https://mystore.com",
"contactEmail": "me@mystore.com",
"stripePublishableKey": "pk_live_...",
"stripeSecretKey": "sk_live_...",
"stripeWebhookSecret": "whsec_...",
"brandColor": "#418a9e"
}
| Field | Type | Required | Description |
|---|---|---|---|
| storeId | string | required | 3-32 chars, lowercase alphanumeric + hyphens |
| name | string | optional | Display name |
| domain | string | optional | Your storefront URL |
| contactEmail | string | optional | Store contact email |
| stripePublishableKey | string | optional | Stripe publishable key (pk_live_ or pk_test_) |
| stripeSecretKey | string | optional | Stripe secret key (sk_live_ or sk_test_) |
| stripeWebhookSecret | string | optional | Stripe webhook secret (whsec_) |
| brandColor | string | optional | Hex color for widget branding |
Response (201)
{
"store": { ... },
"apiKey": "sc_live_abc123...",
"message": "Store created successfully. Save your API key."
}
Request Body
{
"name": "New Store Name",
"domain": "https://newdomain.com",
"brandColor": "#4292e7",
"allowedDomains": ["mystore.com", "www.mystore.com"]
}
Response
{
"apiKey": "sc_live_new_key_here...",
"message": "API key regenerated. Old key is now invalid."
}
Products
Request Body
{
"name": "Trucker Surf Hat",
"price": 2800,
"description": "Classic trucker hat with embroidered surf logo.",
"categoryId": "abc-123",
"compareAtPrice": 3500,
"inventory": 50,
"images": ["https://..."],
"variants": [
{
"variantId": "d90da05a",
"name": "Adjustable / Navy",
"price": 2800,
"sku": "TSH-ADJ-NVY",
"inventory": 25,
"optionValues": { "Size": "Adjustable", "Color": "Navy" }
}
],
"options": [
{ "name": "Size", "values": ["Adjustable"] },
{ "name": "Color", "values": ["Navy", "Black"] }
],
"productType": "physical",
"taxable": true,
"tags": ["hats"],
"status": "active",
"featured": false
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Product name |
| price | integer | required | Price in cents |
| description | string | optional | HTML description |
| categoryId | string | optional | Category UUID |
| compareAtPrice | integer | optional | Original price for sale display (must be > price) |
| inventory | integer | optional | Stock quantity. Null = unlimited. |
| images | array | optional | Array of image URLs |
| variants | array | optional | 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) |
Response (201)
{
"product": {
"productId": "dd9392ed-...",
"slug": "trucker-surf-hat",
"name": "Trucker Surf Hat",
...
}
}
Request Body
{
"productId": "dd9392ed-ea5a-494c-adbc-a8ce5288c2e8",
"price": 3200,
"inventory": 40,
"status": "active"
}
| Field | Type | Required | Description |
|---|---|---|---|
| productId | string | required | Product UUID to update |
All other fields from Create are accepted. Only provided fields are updated.
Request Body
{
"productId": "dd9392ed-ea5a-494c-adbc-a8ce5288c2e8"
}
Categories
parentId to create a subcategory under an existing parent category.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Category name |
| description | string | optional | Category description |
| image | string | optional | Image URL (use the upload endpoint to get a URL first) |
| parentId | string | optional | Set to a parent categoryId to make this a subcategory. Omit or send empty string for top-level. |
| sortOrder | number | optional | Display order (default: 0) |
| status | string | optional | active (default) or hidden |
Example: Top-level category
{
"name": "Men's",
"description": "Men's clothing and accessories",
"image": "https://...",
"sortOrder": 0
}
Example: Subcategory
{
"name": "Shirts",
"description": "Men's shirts",
"parentId": "cat-001",
"image": "https://...",
"sortOrder": 0
}
categoryId which is always required).Updatable Fields
name, slug, description, image, parentId, sortOrder, status
Example: Move to subcategory and update sort order
{
"categoryId": "cat-002",
"parentId": "cat-001",
"sortOrder": 2
}
Example: Promote to top-level
{
"categoryId": "cat-002",
"parentId": ""
}
{
"categoryId": "abc-123"
}
Coupons
{
"coupons": [
{
"code": "SAVE20",
"type": "percent_off",
"value": 20,
"minOrderAmount": 5000,
"maxUses": 100,
"currentUses": 42,
"expiresAt": "2026-12-31T23:59:59Z",
"status": "active"
}
]
}
{
"code": "SAVE20",
"type": "percent_off",
"value": 20,
"minOrderAmount": 5000,
"maxUses": 100,
"expiresAt": "2026-12-31T23:59:59Z",
"status": "active"
}
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | required | Coupon code (auto-uppercased) |
| type | string | required | percent_off, fixed_amount, or free_shipping |
| value | integer | required | Percentage (1-100) or cents amount |
| minOrderAmount | integer | optional | Minimum subtotal in cents |
| maxUses | integer | optional | Max number of times coupon can be used |
| expiresAt | string | optional | ISO datetime when coupon expires |
{
"code": "SAVE20",
"value": 25,
"maxUses": 200
}
{
"code": "SAVE20"
}
Orders
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status (e.g. paid, shipped) |
| from | string | ISO datetime to filter from |
| to | string | ISO datetime to filter to |
| limit | integer | Results per page (default 50) |
| lastKey | string | Pagination token |
Response
{
"orders": [
{
"orderId": "a1b2c3d4-...",
"orderNumber": "MYS-1001",
"email": "customer@example.com",
"customerName": "John Doe",
"items": [...],
"subtotal": 5600,
"shippingCost": 499,
"taxAmount": 336,
"discountAmount": 0,
"total": 6435,
"status": "paid",
"trackingNumber": null,
"createdAt": "2026-02-24T12:00:00Z"
}
],
"lastKey": null
}
{
"status": "shipped",
"trackingNumber": "1Z999AA10123456784",
"notes": "Shipped via UPS"
}
Settings
{
"settings": {
"tax_config": {
"enabled": true,
"useStripeTax": false,
"defaultRate": 6
},
"shipping_config": {
"method": "flat_rate",
"flatRate": 499,
"freeShippingEnabled": false,
"freeShippingThreshold": 5000,
"defaultItemShippingCost": 499,
"tiers": [],
"localPickupEnabled": false,
"localPickupInstructions": ""
},
"email_config": {
"fromEmail": "noreply@mystore.com",
"fromName": "My Store"
},
"order_config": {
"orderPrefix": "MYS",
"nextOrderNumber": 1001
}
}
}
{
"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 |
flat_rate (single rate), per_item (per product), tiered (by subtotal), free. Free tier stores are limited to flat_rate and free.Reports
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| from | string | 2020-01-01 | Start date (ISO) |
| to | string | now | End date (ISO) |
| status | string | paid,shipped,delivered | Comma-separated order statuses to include |
Response
{
"period": { "from": "2026-02-01", "to": "2026-02-28" },
"totalRevenue": 523400,
"totalOrders": 156,
"averageOrderValue": 3355,
"totalTaxCollected": 28450,
"taxByState": { "SC": 18900, "NC": 6200 },
"totalShipping": 45200,
"totalDiscounts": 12300,
"topProducts": [
{ "productId": "...", "name": "Trucker Hat", "unitsSold": 156, "revenue": 155844 }
]
}
Uploads
fileUrl when creating/updating products or categories.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| fileName | string | required | Original file name |
| contentType | string | required | MIME type (e.g. image/jpeg, image/png) |
| uploadType | string | required | product_image, digital_file, category_image, or storefront_image |
| fileSizeMB | number | required | File size in megabytes (validated against tier limits) |
Upload Types & S3 Paths
| uploadType | S3 Path | Use For |
|---|---|---|
product_image | {storeId}/products/{file} | Product images array |
digital_file | {storeId}/digital/{file} | Downloadable digital products |
category_image | {storeId}/categories/{file} | Category/subcategory card images |
storefront_image | {storeId}/storefront/{file} | Hero images, about page images, logo |
Example
{
"fileName": "product-photo.jpg",
"contentType": "image/jpeg",
"uploadType": "product_image",
"fileSizeMB": 2.5
}
Response
{
"uploadUrl": "https://shoppingcart-assets.s3.amazonaws.com/...?X-Amz-Signature=...",
"fileUrl": "https://shoppingcart-assets.s3.amazonaws.com/my-store/products/abc.jpg",
"s3Key": "my-store/products/abc.jpg"
}
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
Response
{
"storefront": {
"enabled": true,
"template": "classic",
"logo": "https://...",
"showStoreName": true,
"accentColor": "#418a9e",
"backgroundColor": "#FFFFFF",
"textColor": "#333333",
"heroTitle": "Welcome to My Store",
"heroLayout": "center",
"heroImage": "",
"storeDescription": "Your store description",
"aboutTitle": "About My Store",
"aboutContent": "About page content...",
"aboutImage": "",
"announcementBar": {
"enabled": false,
"text": "",
"backgroundColor": "#418a9e",
"textColor": "#FFFFFF"
},
"socialLinks": {
"instagram": "",
"twitter": "",
"facebook": "",
"tiktok": "",
"youtube": ""
},
"showPoweredBy": true
}
}
Updatable Fields
| Field | Type | Description |
|---|---|---|
| template | string | Template name: classic, bold, compact |
| logo | string | Store logo URL (recommended 240×72px). Use upload endpoint with storefront_image type. |
| showStoreName | boolean | Show store name text next to logo in nav (default: true) |
| accentColor | string | Primary accent color hex (e.g. #418a9e) |
| backgroundColor | string | Page background color hex |
| textColor | string | Body text color hex |
| heroTitle | string | Hero section heading (defaults to store name) |
| heroLayout | string | center (default), image-left, or image-right |
| heroImage | string | Hero image URL (used with image-left/image-right layouts) |
| storeDescription | string | Hero description text (displayed below title) |
| aboutTitle | string | About page heading (defaults to "About {storeName}") |
| aboutContent | string | About page body content (line breaks preserved) |
| aboutImage | string | About page image URL |
| announcementBar | object | { enabled, text, backgroundColor, textColor } |
| socialLinks | object | { instagram, twitter, facebook, tiktok, youtube } |
| showPoweredBy | boolean | Show "Powered by Nanocart" in footer |
| favicon | string | Favicon URL |
Example
{
"heroTitle": "Welcome to My Shop",
"heroLayout": "image-right",
"heroImage": "https://...",
"accentColor": "#e74c3c",
"aboutTitle": "Our Story"
}
Tier & Usage
{
"tier": "standard",
"limits": {
"maxProducts": 25,
"maxMonthlyOrders": 500,
"maxCoupons": 5,
"maxUploadMB": 25,
"allowedShippingMethods": ["flat_rate", "per_item", "tiered", "free"],
"localPickup": true
},
"usage": {
"activeProducts": 12,
"activeCoupons": 2,
"monthlyOrders": 47
}
}
Subscriptions
{
"tier": "pro",
"billingPeriod": "monthly"
}
| Field | Type | Options |
|---|---|---|
| tier | string | standard, pro, expert |
| billingPeriod | string | monthly, annual |
Response
{
"sessionUrl": "https://checkout.stripe.com/c/pay/..."
}
{
"tier": "pro",
"subscriptionId": "sub_...",
"subscriptionStatus": "active",
"billingPeriod": "monthly",
"cancelAtPeriodEnd": false
}
{
"message": "Subscription will cancel at end of billing period."
}
Response
{
"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. |
x-nanocart-delivery | Unique delivery ID (UUID) for idempotency |
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const sig = req.headers['x-nanocart-signature'] || '';
const ts = req.headers['x-nanocart-timestamp'] || '';
// Reject stale deliveries
if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody) // raw Buffer/string, before JSON.parse
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post('/webhooks/nanocart', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyWebhook(req, process.env.NANOCART_WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
// handle event.type ...
res.sendStatus(200);
});
Event Types
All events share a common envelope:
{
"eventType": "order.created",
"storeId": "my-store",
"timestamp": "2026-06-17T14:00:00Z",
"data": { ... }
}
{
"eventType": "order.created",
"storeId": "my-store",
"timestamp": "2026-06-17T14:00:00Z",
"data": {
"orderId": "a1b2c3d4-...",
"orderNumber": "MYS-1001",
"email": "customer@example.com",
"customerName": "John Doe",
"items": [
{ "productId": "...", "name": "Trucker Hat", "variantName": "Navy", "price": 2800, "quantity": 2 }
],
"subtotal": 5600,
"shippingCost": 499,
"taxAmount": 336,
"discountAmount": 0,
"total": 6435,
"status": "paid",
"createdAt": "2026-06-17T14:00:00Z"
}
}
shipped (via PUT /admin/orders/{orderId}).{
"eventType": "order.shipped",
"storeId": "my-store",
"timestamp": "2026-06-17T16:00:00Z",
"data": {
"orderId": "a1b2c3d4-...",
"orderNumber": "MYS-1001",
"email": "customer@example.com",
"trackingNumber": "1Z999AA10123456784",
"status": "shipped"
}
}
cancelled.{
"eventType": "order.cancelled",
"storeId": "my-store",
"timestamp": "2026-06-17T17:00:00Z",
"data": {
"orderId": "a1b2c3d4-...",
"orderNumber": "MYS-1001",
"email": "customer@example.com",
"status": "cancelled"
}
}
{
"eventType": "product.low_stock",
"storeId": "my-store",
"timestamp": "2026-06-17T14:01:00Z",
"data": {
"productId": "dd9392ed-...",
"productName": "Trucker Surf Hat",
"slug": "trucker-surf-hat",
"variantId": "d90da05a",
"variantName": "Adjustable / Navy",
"inventory": 3
}
}
{
"eventType": "checkout.abandoned",
"storeId": "my-store",
"timestamp": "2026-06-17T14:35:00Z",
"data": {
"orderId": "a1b2c3d4-...",
"email": "customer@example.com",
"items": [
{ "productId": "...", "name": "Trucker Hat", "quantity": 1, "price": 2800 }
],
"subtotal": 2800
}
}
{
"eventType": "webhook.test",
"storeId": "my-store",
"timestamp": "2026-06-17T14:00:00Z",
"data": {
"message": "This is a test event from nanocart."
}
}
Error Codes
Error responses include an error message and an optional code for programmatic handling:
{
"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 |
"tierLimit": true so your app can detect upgrade prompts. The error message includes the current limit and the next tier's limit.© 2026 NanoCart · a ByteBunny, LLC company | Dashboard | hello@nanocart.io
Last updated: July 2026