npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

epicmerch

v1.3.0

Published

Official JavaScript SDK for EpicMerch Storefronts

Readme

🛍️ EpicMerch SDK

The official JavaScript SDK for building custom storefronts with EpicMerch.

npm version license npm downloads

Products · Auth · Cart · Orders · Payments · Notifications · Analytics


✨ What is EpicMerch?

EpicMerch is a multi-tenant e-commerce platform. This SDK lets you connect any website to your EpicMerch store — fetch products, handle authentication, manage carts, process Razorpay payments, and more — all with a single import.


📦 Installation

npm install epicmerch

HTML / CDN (no bundler needed):

<script src="https://unpkg.com/epicmerch/dist/epicmerch.min.js"></script>

🚀 Quick Start

import EpicMerch from 'epicmerch';

const store = new EpicMerch({
  apiKey: 'pk_live_YOUR_API_KEY'
});

// List products
const { products } = await store.products.list();
console.log(products);

// Subscribe a user
await store.newsletter.subscribe('[email protected]');

Get your API key: Log into your EpicMerch Dashboard → Settings → API Keys → Generate New Key


🔧 Initialization

const store = new EpicMerch({
  apiKey: 'pk_live_YOUR_API_KEY',       // required
  baseUrl: 'http://localhost:5001/api'  // optional — only for local dev
});

| Option | Type | Required | Description | |--------|------|----------|-------------| | apiKey | string | ✅ Yes | Your merchant public API key | | baseUrl | string | ❌ No | Override API URL (dev only) | | onAuthError | function | ❌ No | Callback when token expires |


🔐 Authentication

OTP Login (Phone or Email)

// Step 1 — Send OTP
await store.auth.sendOtp('+919876543210', 'phone');
// or: await store.auth.sendOtp('[email protected]', 'email');

// Step 2 — Verify OTP
const result = await store.auth.verifyOtp('+919876543210', '123456', {
  name: 'Jane Doe',           // for new users
  email: '[email protected]'   // optional
});

// Token is auto-stored in the SDK
console.log(result.token, result.user);

Google OAuth

// Redirect to Google
const url = store.auth.getGoogleAuthUrl(window.location.origin + '/login');
window.location.href = url;

// On the callback page
const result = await store.auth.handleOAuthCallback();
if (result) {
  console.log('Logged in as', result.user.name);
}

Session & Token Management

// Restore session on page load
const savedUser = JSON.parse(localStorage.getItem('customerInfo'));
if (savedUser?.token) {
  store.setCustomerToken(savedUser.token);
}

// Check if session is still valid
const session = await store.auth.getSession();

// Refresh before expiry (tokens last 30 min)
const refreshed = await store.auth.refreshToken();

// Logout
store.auth.logout();
store.clearCustomerToken();

🛍️ Products

// List all products
const { products, total, pages } = await store.products.list();

// With filters
const { products } = await store.products.list({
  type: 'Apparel',   // category filter
  keyword: 'hoodie', // search
  sort: 'newest',    // 'newest' | 'popularity' | 'price_asc' | 'price_desc'
  page: 1,
  limit: 12
});

// Get a single product
const product = await store.products.get('PRODUCT_ID');
console.log(product.name, product.price, product.variants);

// Search
const { products } = await store.products.search('graphic tee', { limit: 6 });

🗂️ Categories

// Get all visible categories (includes 'All')
const categories = await store.categories.list();
// → ['All', 'Apparel', 'Accessories', 'Headwear']

🛒 Shopping Cart

Requires customer to be logged in.

// Get cart — server returns ONLY { cart: [...] }.
// Each line item: { product: { _id, name, price, originalPrice,
// salePrice, image, type }, qty, variant }.
// `product.price` on cart items is ALREADY the effective price
// (sale if on sale, else regular). Compute count/total locally:
//   const count = cart.length;
//   const total = cart.reduce((s, i) => s + i.product.price * i.qty, 0);
const { cart } = await store.cart.get();

// Add item (with optional size/variant — variant is a STRING, not an object).
// Response: { message, cartCount } — useful for header badge updates.
await store.cart.add('PRODUCT_ID', 1, 'L');

// Update quantity (variant required when the product has variants)
await store.cart.update('PRODUCT_ID', 3, 'L');

// Remove item (variant required to disambiguate if the same product
// is in cart in multiple sizes)
await store.cart.remove('PRODUCT_ID', 'L');

// Clear entire cart
await store.cart.clear();

📋 Orders

Requires customer to be logged in.

// Create order
// Each line item uses `productId` — the same field name as cart.add,
// magic-checkout-init, analytics.track, etc. `product` is still accepted
// as a legacy alias by the server (back-compat) but `productId` is the
// canonical, consistent name across all endpoints.
const order = await store.orders.create({
  orderItems: [
    {
      productId: 'PRODUCT_ID',
      name: 'Classic Tee',
      image: 'https://...',
      price: 999,
      qty: 2,
      variant: 'L'
    }
  ],
  shippingAddress: {
    fullName: 'Jane Doe',
    address: '123 Main St',
    city: 'Mumbai',
    state: 'Maharashtra',
    postalCode: '400001',
    country: 'India',
    phone: '+919876543210'
  },
  paymentMethod: 'Razorpay',  // or 'COD'
  totalPrice: 1998
});

// List my orders
const orders = await store.orders.list();

// Get a specific order
const order = await store.orders.get('ORDER_ID');

// Cancel order
await store.orders.cancel('ORDER_ID');

// Calculate total (helper)
const total = store.orders.calculateTotal([
  { price: 500, qty: 2 },
  { price: 300, qty: 1 }
]); // → 1300

🔁 Idempotency (Prevent Duplicate Orders)

// Generate a stable key once per checkout session
const key = store.generateIdempotencyKey();

// Pass it to prevent double-clicks from creating two orders
const order = await store.orders.create(orderData, { idempotencyKey: key });

if (order._idempotent) {
  console.log('Duplicate request — order already exists:', order.orderId);
}

📍 Addresses

Requires customer to be logged in.

// List saved addresses
const { addresses } = await store.addresses.list();

// Add address
await store.addresses.add({
  fullName: 'Jane Doe',
  address: '123 Main St',
  city: 'Mumbai',
  state: 'Maharashtra',
  postalCode: '400001',
  country: 'India',
  phone: '+919876543210',
  isDefault: true
});

// Remove address
await store.addresses.remove('ADDRESS_ID');

// Update phone / profile
await store.addresses.updateProfile({
  name: 'Jane Doe',
  phoneNumber: '+919999988888'
});

💳 Payments (Razorpay)

Standard Payment Flow (single call)

orders.create() creates both the EpicMerch order AND the Razorpay payment order in one transaction, and returns every field the Razorpay checkout widget needs. Do NOT also call payment.getConfig() + payment.createOrder() — that creates an orphan second Razorpay order and triggers an idempotency-key collision against /customer/orders. Open the widget straight from the orders.create response.

// 1. Create the order — the response carries the Razorpay session.
const order = await store.orders.create({ ...orderData });
// order = { orderId, trackingNumber, razorpayOrderId, razorpayKeyId,
//           amount, currency, merchantName }

// 2. Open Razorpay checkout using fields from that response.
const rzp = new Razorpay({
  key:      order.razorpayKeyId,
  order_id: order.razorpayOrderId,
  amount:   order.amount,
  currency: order.currency,
  name:     order.merchantName,
  handler: async (response) => {
    // 3. Verify the payment (via SDK).
    const result = await store.payment.verify({
      razorpay_order_id: response.razorpay_order_id,
      razorpay_payment_id: response.razorpay_payment_id,
      razorpay_signature: response.razorpay_signature,
      orderId: order.orderId
    });
    await store.cart.clear();
    console.log('Payment verified ✅', result);
  }
});
rzp.open();

payment.getConfig() and payment.createOrder() still exist for advanced flows where the EpicMerch order was created out-of-band (e.g. server-to-server), but the normal storefront path is the single orders.create call above.

Saved Payment Methods (One-Click Checkout)

// Get saved cards / UPI
const methods = await store.payment.getSavedMethods();

// Save after successful payment (with user consent)
await store.payment.saveMethod({ paymentId: 'pay_...', type: 'card' });

// Set a method as default
await store.payment.setDefault('METHOD_ID');

// Remove a saved method
await store.payment.removeMethod('METHOD_ID');

// Charge a saved method directly (one-click)
const result = await store.payment.chargeSaved('METHOD_ID', 999, 'ORDER_ID');

📬 Notifications

Send emails or WhatsApp messages directly from the storefront.

// Send to the logged-in customer
await store.notifications.sendToMe({
  subject: 'Order Confirmed!',
  message: 'Your order #1234 is confirmed.',
  type: 'email'  // or 'whatsapp'
});

// Send to any email or phone
await store.notifications.send({
  to: '[email protected]',   // or phone number for WhatsApp
  subject: 'Your shipment is on the way 🚚',
  message: 'Track your order at epicthreadz.in/track-order',
  type: 'email'
});

Abandoned Cart Recovery

// Find users with items in cart for 24+ hours
const { abandonedCarts } = await store.notifications.getAbandonedCarts(24);

// Send bulk recovery messages (supports {name}, {cartItems}, {cartValue})
const result = await store.notifications.notifyAbandonedCarts({
  subject: 'You left something behind! 🛒',
  message: 'Hey {name}, your {cartItems} items worth ₹{cartValue} are waiting!',
  type: 'whatsapp',
  hours: 24
});
console.log(`Sent to ${result.sentCount} customers`);

📧 Newsletter

await store.newsletter.subscribe('[email protected]', 'Jane');

📊 Analytics

Track customer behaviour silently — errors never break the UI.

// Page view (call on every route change)
await store.analytics.track({
  eventType: 'page_view',
  url: '/products',
  sessionId: 'sess_abc123'
});

// Product view
await store.analytics.track({
  eventType: 'product_view',
  productId: 'PRODUCT_ID',
  productName: 'Classic Tee',
  sessionId: 'sess_abc123'
});

// Add to cart
await store.analytics.track({
  eventType: 'add_to_cart',
  productId: 'PRODUCT_ID',
  quantity: 1,
  sessionId: 'sess_abc123'
});

// Checkout initiated
await store.analytics.track({ eventType: 'checkout', sessionId: 'sess_abc123' });

// Purchase complete
await store.analytics.track({
  eventType: 'purchase',
  orderId: 'ORDER_ID',
  orderTotal: 1998,
  sessionId: 'sess_abc123'
});

Event types: page_view · product_view · add_to_cart · checkout · purchase


⚠️ Error Handling

try {
  const order = await store.orders.create(orderData);
} catch (error) {
  if (error.message.includes('401')) {
    // Token expired — redirect to login
  } else if (error.message.includes('409')) {
    // Conflict — request already being processed
  } else if (error.message.includes('429')) {
    // Rate limited — wait and retry
  } else {
    console.error('Error:', error.message);
  }
}

| Status | Meaning | |--------|---------| | 401 | Token expired or invalid — re-authenticate | | 403 | Forbidden — wrong API key | | 404 | Resource not found | | 409 | Conflict — duplicate request in progress | | 429 | Rate limited — too many requests |

Error response shape

Errors return a structured JSON envelope you can branch on:

{
  "success": false,
  "code": "INSUFFICIENT_STOCK",
  "message": "Classic Tee (L): need 2, have 1",
  "hint": "Reduce the quantity, pick another variant, or check the product's stock.",
  "items": [{ "product": "Classic Tee", "variant": "L", "requested": 2, "available": 1 }]
}

Branch on code, not on substring-matching message (messages are human-readable and may change). Common codes:

| code | When | |--------|------| | INSUFFICIENT_STOCK | A line item lacks stock for the requested variant/qty. items[] lists each shortfall. | | INVALID_ORDER_ITEM | An orderItems[] entry is missing productId. received shows what you sent. | | NO_ORDER_ITEMS | The orderItems array was empty. | | PRODUCT_NOT_FOUND | The productId doesn't match any product in this store. | | PAYMENT_NOT_CONFIGURED | Store has no Razorpay keys yet — can't take card payments (COD still works). | | PAYMENT_INIT_FAILED | Razorpay rejected order creation. The EpicMerch order was rolled back — nothing charged. | | RATE_LIMIT_EXCEEDED (+ ORDER_/OTP_/PAYMENT_/STOCK_/AUTH_ variants) | You hit a rate limit. See the table below. |


⏱️ Rate Limits

All limits are enforced per merchant (by API key / tenant) or per customer where noted. Exceeding a limit returns 429 with { code, message, retryAfter } plus standard RateLimit-* response headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset).

| Operation | Limit | Window | Code | |-----------|-------|--------|------| | General API | 1000 req (configurable per merchant) | 1 min | RATE_LIMIT_EXCEEDED | | Auth (login) | 10 attempts | 15 min | AUTH_RATE_LIMIT_EXCEEDED | | OTP send | 5 requests | 1 hour | OTP_RATE_LIMIT_EXCEEDED | | Order creation | 5 orders (per customer/key) | 1 min | ORDER_RATE_LIMIT_EXCEEDED | | Payment ops | 10 attempts (per customer/key) | 1 min | PAYMENT_RATE_LIMIT_EXCEEDED | | Stock ops (add-to-cart/checkout) | 20 (per customer) | 10 sec | STOCK_RATE_LIMIT_EXCEEDED |

Honest notes:

  • The general 1000/min limit is the default — a merchant can raise or lower it (or disable it) from their security settings (securitySettings.rateLimitPerMinute / rateLimitEnabled).
  • Localhost requests are not rate-limited in development, so local testing won't trip these.
  • Always read retryAfter (seconds) from a 429 body and back off rather than hammering — the order/payment/stock limiters exist to protect inventory integrity, not to annoy you.
// Respect 429 backoff
async function withRetry(fn, max = 3) {
  for (let attempt = 0; ; attempt++) {
    try { return await fn(); }
    catch (e) {
      const retryAfter = e?.body?.retryAfter ?? 2 ** attempt;
      if (attempt >= max || !String(e.message).includes('429')) throw e;
      await new Promise(r => setTimeout(r, retryAfter * 1000));
    }
  }
}

🧩 React / Next.js Integration Example

// lib/store.js — create singleton
import EpicMerch from 'epicmerch';
export const store = new EpicMerch({ apiKey: process.env.NEXT_PUBLIC_API_KEY });

// components/ProductGrid.jsx
import { store } from '../lib/store';
import { useEffect, useState } from 'react';

export default function ProductGrid() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    store.products.list({ limit: 12, sort: 'newest' })
      .then(data => setProducts(data.products));
  }, []);

  return (
    <div className="grid grid-cols-3 gap-6">
      {products.map(p => (
        <div key={p._id}>
          <img src={p.images[0]} alt={p.name} />
          <h3>{p.name}</h3>
          <p>₹{p.price}</p>
          <button onClick={() => store.cart.add(p._id, 1)}>Add to Cart</button>
        </div>
      ))}
    </div>
  );
}

📋 Full API Reference

| Namespace | Method | Auth Required | |-----------|--------|:---:| | store.products | .list(options) · .get(id) · .search(query) | — | | store.categories | .list() | — | | store.auth | .sendOtp() · .verifyOtp() · .getGoogleAuthUrl() · .handleOAuthCallback() · .getSession() · .refreshToken() · .logout() | — | | store.cart | .get() · .add() · .update() · .remove() · .clear() | ✅ | | store.orders | .create() · .list() · .get() · .cancel() · .calculateTotal() | ✅ | | store.addresses | .list() · .add() · .remove() · .updateProfile() | ✅ | | store.payment | .getConfig() · .createOrder() · .verify() · .getSavedMethods() · .saveMethod() · .removeMethod() · .setDefault() · .chargeSaved() | ✅ | | store.newsletter | .subscribe(email, name) | — | | store.notifications | .sendToMe() · .send() · .getAbandonedCarts() · .notifyAbandonedCarts() | ✅ | | store.analytics | .track(event) | — |


💳 Payments (Stripe)

Stripe (when merchant has checkoutType: 'stripe')

const config = await sdk.getStripeConfig();
// → { processor: 'stripe', publishableKey: 'pk_live_xxx' }

const intent = await sdk.createStripePaymentIntent({ amount: 25.50, orderId: 'order_42' });
// → { clientSecret: 'pi_xxx_secret_yyy', intentId: 'pi_xxx' }

Pair with @stripe/stripe-js + @stripe/react-stripe-js on the storefront. See epic-threadz/src/pages/Checkout.jsx for a reference integration. Amount is in major units (e.g. dollars), not cents — the backend handles the conversion.


🌐 Browser Support

Works in all modern browsers. For legacy support, add a fetch polyfill.


📄 License

MIT — © EpicMerch