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

@usestorekit/sdk

v0.8.1

Published

Type-safe, ergonomic SDK for the StoreKit Storefront API

Readme

@usestorekit/sdk

Type-safe, ergonomic SDK for the StoreKit Storefront API. Namespaced calls, a { data, error } result on every method (never throws by default), fully typed requests/responses — and a Next.js adapter that wires the whole storefront (cookie session, cart, catalog caching, a same-origin proxy, and React hooks) in about two minutes.

Browser ──► your Next.js app (proxy + cache) ──► StoreKit Storefront API

Your store key never reaches the browser: the browser calls a catch-all route on your own app, which injects the key and the customer's session cookie server-side.


Install

npm install @usestorekit/sdk zod
# or: pnpm add @usestorekit/sdk zod

react and next are optional peers — needed only for the /react and /next entries.

Next.js quickstart (≈2 min)

1. Environment

# .env.local
STOREFRONT_STORE_KEY=sk_live_xxx          # server-only — never NEXT_PUBLIC_*
STOREFRONT_URL=https://shop.your-domain.com  # your storefront's public base URL —
                                             # the SDK's baseURL (post-payment return)
STOREFRONT_API_URL=https://api.your-storekit.com  # optional — defaults to
                                                  # https://api.storekit.app (production).
                                                  # Set only for self-hosted/staging.

Custom (self-hosted) storefronts must set STOREFRONT_URL so the post-payment redirect lands on your domain instead of the default *.storekit.app subdomain. See Payments & the return route.

2. The server instance — lib/storekit.ts

import { initStorekit } from "@usestorekit/sdk/next";

export const storekit = initStorekit(); // reads env, wires cookies + cache

3. The catch-all proxy — app/api/storefront/[...path]/route.ts

import { storekit } from "@/lib/storekit";

export const { GET, POST, PUT, PATCH, DELETE } = storekit.handler();

4. The browser client — lib/storekit-client.ts

"use client";
import { createStorekitClient } from "@usestorekit/sdk/react";

export const storefront = createStorekitClient(); // talks to /api/storefront

5. The payment return route — app/payment/callback/page.tsx

Checkout is a redirect flow: after paying, the customer is returned to /payment/callback on your storefront. This route is required — skip it and a paid order 404s. The usePaymentConfirmation hook handles reconciliation; see Payments & the return route for the page.

That's the entire integration. Now use it:

// Server Component — reads the API directly, public catalog reads auto-cached
import { storekit } from "@/lib/storekit";
const { data } = await storekit.products.list({ limit: 20 });
// Server Action — cart + session cookies handled for you (no cart id juggling)
"use server";
import { storekit } from "@/lib/storekit";

export async function addToCart(variantId: string) {
  await storekit.cart.add({ variantId, quantity: 1 });
}
export async function login(phone: string, otp: string) {
  // claims the guest cart, stores the token in the httpOnly session cookie
  return storekit.auth.verifyOtp(phone, otp);
}
// Client Component — shared-state hooks, proxied through your catch-all route
"use client";
import { storefront } from "@/lib/storekit-client";

export function CartBadge() {
  const { count } = storefront.useCart();
  return <span>Cart ({count})</span>;
}

The two surfaces

| | Function | Import | Talks to | Use in | | --- | --- | --- | --- | --- | | Server | initStorekit() | @usestorekit/sdk/next | StoreKit API directly | RSC, generateMetadata, Server Actions — and it backs the catch-all route | | Browser | createStorekitClient() | @usestorekit/sdk/react | your same-origin /api/storefront/* | Client Components |

initStorekit(config?)storekit

storekit.products / categories / pages / store / search / coupons / checkout / customer / payment
                              // direct, typed, public reads auto-cached
storekit.cart                 // cookie-stateful: current() · add(item) · setQuantity(lineId, n)
                              //                  · remove(lineId) · clear() · count()
storekit.auth                 // cookie-stateful: requestOtp(phone) · verifyOtp(phone, otp)
                              //                  · logout() · session()
storekit.handler()            // the catch-all Route Handlers
storekit.$client              // escape hatch: the framework-agnostic client

config (all optional): apiURL, storeKey, outletId, baseURL (your storefront's public URL — where customers return after payment), revalidate (catalog cache window, default 300s), cookies (session / cart names), fetch. Defaults come from STOREFRONT_API_URL / STOREFRONT_STORE_KEY / STOREFRONT_OUTLET_ID / STOREFRONT_URL. apiURL falls back to https://api.storekit.app (production) when unset, so only self-hosted/staging integrations need to set it.

Cookie writes (cart.add, auth.verifyOtp, …) require a Server Action or Route Handler — Next can't set cookies during render. Reads (cart.current, customer.get) work anywhere.

createStorekitClient(config?)storefront

storefront.cart    // get() · add(item) · setQuantity(lineId, n) · remove(lineId) · clear()
storefront.auth    // requestOtp(phone) · verifyOtp(phone, otp) · logout() · session()
storefront.products / categories / pages / store / search / coupons / checkout / customer / payment
storefront.useCart()     // { cart, count, loading, error, add, setQuantity, remove, clear, refresh }
storefront.useStore()    // { data, error, loading }
storefront.useSession()  // { data: customer | null, error, loading, refresh, update(input) }
storefront.useAddresses()// { addresses, loading, error, refresh, create, update, remove }
storefront.usePaymentConfirmation(orderId)  // { status, order, error, message, retry } — for /payment/callback

useCart() shares one cart state across every component using the same client, so a header badge updates the instant another component adds an item — no polling. Config: basePath (default /api/storefront), fetch, headers.

Mutations are optimistic. useCart (add / setQuantity / remove / clear), useSession().update, and useAddresses (create / update / remove) apply locally right away, then reconcile with the server and roll back on error. The cart subtotal, line totals, and count update instantly; the grand total and charges (tax, packaging, delivery) stay server-authoritative and settle on reconcile — they can depend on thresholds the browser can't reproduce. Rapid taps are safe: a slow in-flight response can't overwrite newer state.

First adds are optimistic too — automatically. To render a brand-new line the SDK needs the variant's display data (name / price / image), which it can't derive from a variant id alone. It harvests that data from every catalog read it already proxies (products.list / products.get / search) into a small in-memory index, so a first add({ variantId, quantity }) synthesizes its line instantly with no extra arguments — as long as the variant was loaded through the client.

For the cold case — a server-rendered product page where the browser never fetched the catalog itself — pass an optimistic hint to guarantee a synchronous first add. Build it from the product + variant you already have with lineHint:

import { lineHint } from "@usestorekit/sdk/react";

const { add } = storefront.useCart();
await add({ variantId: variant.id, quantity: 1 }, lineHint(product, variant));

// pass selected modifiers so their prices are folded into the optimistic unit price:
await add({ variantId, quantity: 1, modifiers }, lineHint(product, variant, { modifiers: selected }));

Without a hint and with a cold index (or before the cart has loaded), the add falls back to a count-only bump and the row arrives on reconcile.

What the proxy does

storekit.handler() mounts three things at your route:

  • Cart facade (/cart, /cart/items[/:lineId]) — cookie-stateful, so the browser never sees or sends a cart id.
  • Auth facade (/auth/otp/*, /auth/logout, /auth/session) — sets/clears the httpOnly session cookie; the bearer token is never echoed to the browser.
  • Transparent /v1/* proxy — injects the store key + session token, caches public catalog reads, and forwards everything else with no-store. Only /v1/* is forwardable; path traversal is rejected.

Payments (PhonePe) & the return route

Checkout is a redirect flow, and it needs one route on your storefront that the quickstart above doesn't create. Here's the round-trip:

checkout.create()  ──►  PhonePe hosted page  ──►  StoreKit API  ──►  YOUR /payment/callback
   (you navigate          (customer pays)         (resolves the      (confirm + show result)
    to redirectUrl)                                store + status)
  1. checkout.create() returns a PhonePe redirectUrl; navigate the browser to it.
  2. After payment, PhonePe returns to the StoreKit API, which 302s the customer to <your-storefront>/payment/callback?orderId=…&status=….
  3. That page calls checkout.confirm(orderId) to reconcile (the webhook may have already settled the order) and renders success / pending / failure.

The /payment/callback path is required — without it, a successfully paid order lands on a 404. The usePaymentConfirmation hook does the confirm + retry + status machine; you supply the markup:

// app/payment/callback/page.tsx
"use client";
import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { storefront } from "@/lib/storekit-client";
import { formatMoney } from "@usestorekit/sdk/react";

function Result() {
  const orderId = useSearchParams().get("orderId");
  const { status, order, message, retry } = storefront.usePaymentConfirmation(orderId);

  if (status === "processing") return <p>Confirming your payment…</p>;
  if (status === "success")
    return <p>Order placed! Total {order && formatMoney(order.total, order.currency)}.</p>;
  if (status === "pending")
    return (
      <div>
        <p>{message ?? "Payment is still processing."}</p>
        <button onClick={retry}>Check again</button>
      </div>
    );
  return (
    <div>
      <p>{message ?? "Payment could not be confirmed."}</p>
      <button onClick={retry}>Retry</button>
    </div>
  );
}

// useSearchParams must be inside <Suspense>
export default function Page() {
  return (
    <Suspense>
      <Result />
    </Suspense>
  );
}

Where customers return — baseURL

Tell the SDK your storefront's public base URL, once — the API derives the post-payment redirect (<baseURL>/payment/callback) from it. For integrated storefronts (the default *.storekit.app subdomain) it just works without this. For a custom storefront on your own domain, set it so the redirect comes back to you:

// lib/storekit.ts
export const storekit = initStorekit({
  baseURL: process.env.STOREFRONT_URL, // e.g. https://shop.acme.com (this is the default)
});

The SDK injects it into every checkout server-side (the browser can't override it), the API persists it on the order, and the redirect uses it. Resolution precedence on the API: the order's baseURL → the store's dashboard-configured URL → the store's registered domains. If you don't set it, the SDK warns in the console — explicit is better than a surprise redirect to the wrong origin.

Orders

customer.orders.list({ cursor?, limit? }) returns paginated history (keyset cursor, plus lifetime totalSpent); customer.orders.get(orderId) returns one order. Both carry the full charge breakdown and shipping address, so an order page can itemise totals instead of deriving a remainder:

const { data } = await storefront.customer.orders.get(orderId);
// data.orderNumber                         — per-store human number (e.g. 1042)
// data.subtotal, data.taxAmount, data.deliveryCharge, data.packagingCharge,
//   data.discountAmount, data.total        — all numbers in data.currency
// data.deliveryDistanceKm                  — number | null
// data.shippingAddress / data.billingAddress
//                                          — { firstName, lastName, address, city,
//                                              state, zipCode, country, latitude?,
//                                              longitude? } | null. billingAddress
//                                              defaults to shipping at checkout.

The single-order endpoint also returns payment, refunds, a status timeline, shipments, and a receipt link (the list omits these to stay lean):

data.payment;       // { transactionId, providerRef, amount, paidAt,
                    //   method, last4, upiId, reference } | null — null until paid.
                    //   Instrument fields are best-effort (any may be null).
data.refunds;       // Array<{ id, amount, reason, createdAt }>
data.statusEvents;  // Array<{ fromStatus, toStatus, action, createdAt }> — oldest→newest
data.shipments;     // Array<{ awbCode, carrier, trackingUrl, status, items, … }> (partial-shipment aware)
data.invoiceUrl;    // signed, time-limited link to a printable HTML receipt

Tracking fields (deliveryStatus, deliveryPartner, trackingUrl, estimatedDelivery, deliveredAt, estimatedReadyAt) and notes come through on both. Types: Order, OrderItem, OrderShippingAddress, OrderPayment, OrderRefund, OrderStatusEvent, OrderShipment.

Pass a separate billing address to checkout.create() when it differs from shipping. Still not modelled: order-level taxInclusive (the cart exposes it; the order doesn't yet).

The { data, error } contract

Every method resolves to a discriminated result — it never throws (unless you set throwOnError):

const { data, error } = await storekit.products.get("slug");
if (error) {
  // data is null; error is a StorefrontError ({ message, code, status, retryAfter?, details? })
} else {
  // data is the Product
}
import { isUnauthorized, isRateLimited } from "@usestorekit/sdk/next";

const { error } = await storekit.cart.current();
if (error && isUnauthorized(error)) redirect("/login");

code mirrors the API envelope (UNAUTHORIZED, RATE_LIMITED, NOT_FOUND, route-specific codes like INSUFFICIENT_STOCK, plus NETWORK / TIMEOUT). Guards: isStorefrontError, isRateLimited, isUnauthorized, isNotFound.

Helpers & types

import { formatMoney } from "@usestorekit/sdk";        // locale pinned → no hydration mismatch
import type { Product, Cart, Order, Customer, Address } from "@usestorekit/sdk/next";

formatMoney(amount, currency?, locale?) is exported from all three entries.


Advanced: the framework-agnostic client

@usestorekit/sdk/next is built on a portable core client you can use directly (other frameworks, edge functions, scripts). It takes an explicit baseURL + storeKey and a pluggable SessionStore:

import { createStorefrontClient } from "@usestorekit/sdk";

const sf = createStorefrontClient({
  baseURL: process.env.STOREFRONT_API_URL!,
  storeKey: process.env.STOREFRONT_STORE_KEY!,
  // session, outletId, fetch, credentials, throwOnError, headers, onRequest/onResponse/onError
});

const { data, error } = await sf.products.list({ limit: 20 });

Cart helpers here are explicit about the cart id and operate on lines (cart.items[].id), so variants that differ only by modifiers stay independent:

await sf.cart.create({ items: [{ variantId, quantity: 2 }] });
const { data: cart } = await sf.cart.me();
await sf.cart.setQuantity(cart!.id, cart!.items[0].id, 3);
await sf.cart.addItem(cart!.id, { variantId, quantity: 1 }); // merges same variant + modifiers

createStorefrontClient(config) options: baseURL (required), storeKey, outletId, session (defaults to memorySession()), fetch, credentials, throwOnError, headers, onRequest / onResponse / onError. Per-request options (2nd arg): cache, revalidate, tags, signal, headers, outletId.

Sessions are per-request. memorySession() keeps one token for the client's lifetime — fine for a single user, but don't share a long-lived client across users. On the server use a request-scoped SessionStore (the /next adapter does this for you via cookieSession()).

Resource reference (both clients)

All methods take an optional final RequestOptions.

authrequestOtp · verifyOtp · logout · getToken productslist({ limit?, cursor?, categorySlug?, tagSlug?, status? }) · get(slug) · listAll(query?) cart (core)create · get · me · update · delete · claim · addItem · setQuantity · removeItem checkoutcreate({ orderType?, shipping?, billing?, couponCode?, notes?, scheduledFor?, idempotencyKey? }) · confirm(orderId) customerget · update · orders.list · orders.get · addresses.list/create/update/delete paymentcreateIntent(orderId) · status(orderId) · checkoutUrl(transactionId) — retry/resume + poll an order's payment storeget · outlets · categorieslist · get(slug, query?) · pageslist · get(slug) · searchsearch({ q, … }) · couponsvalidate({ code })

Escape hatches

storekit.$client;                 // the underlying core client (server)
storefront.$core;                 // the underlying core client (browser)
client.$transport.request<T>("GET", "/v1/anything", { query, options }); // raw typed call