> ## Documentation Index
> Fetch the complete documentation index at: https://andcze.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> @itemshop/sdk — keyless storefront, React hooks, the API-key client and the CLI.

`@itemshop/sdk` is the reference ItemShop SDK: zero runtime dependencies, ESM + CJS, works in browsers, Node.js 20+, Bun, Deno and Cloudflare Workers (everything except the CLI uses only `fetch`). The package also ships the [CLI](https://andcze.mintlify.site/en/site/cli.md).

| Import | Where it runs | What it is |
| - | - | - |
| `@itemshop/sdk/storefront` | browser, any JS runtime | `createStorefront()` — shop data and checkout. **No secrets.** |
| `@itemshop/sdk/client` | server only | `createClient({ apiKey })` — the full API with your shop's key |
| `@itemshop/sdk/react` | React 18+ | `StorefrontProvider`, `useShop`, `useProducts`, `useCart`, `useCheckout`… |
| `@itemshop/sdk/vite` | Vite config | plugin that injects `window.__ITEMSHOP__` in `vite dev` |
| `itemshop` (bin) | terminal | `login`, `init`, `dev`, `deploy`, `pull`, `skills`, `mcp` |

`@itemshop/sdk` re-exports both the storefront and the client entry.

## Installation

```bash theme={null}
npm install @itemshop/sdk
```

## Storefront (no key)

Sites hosted by ItemShop get `window.__ITEMSHOP__ = { shopId, apiBase, locale }` injected, so no configuration is needed:

```ts theme={null}
import { createStorefront, ItemShopError } from "@itemshop/sdk/storefront";

const sf = createStorefront(); // or createStorefront({ shopId, apiBase, locale }) / ({ domain: "sklep.example.pl" })

const shop = await sf.shop.get();                          // displayName, logo, currency, languages, servers (isOnline), legal…
const { products, categories } = await sf.products.list({ category: categoryId }); // category = server
const vip = await sf.products.get("ranga-vip");            // slug or id
const { name } = sf.localize(vip);                         // translation with fallback
const methods = await sf.paymentMethods.list();            // one per channel: { code, channel, name, icon, testMode, limits }

const quote = await sf.discounts.validate("LATO10", [{ productId: vip.id }]);

try {
  const { redirectUrl, orderNumber, statusKey } = await sf.checkout.create({
    items: [{ productId: vip.id, quantity: 1 }],           // 1–20 lines; 2+ = one payment group
    nick: "Steve",                                         // game shops; general shops use `email`
    email: "steve@example.com",
    discountCode: quote.valid ? "LATO10" : undefined,
    consents: { terms: true, immediateDelivery: true, legalVersion: shop.legal.version },
    paymentMethodCode: methods[0]!.code,
    paymentChannel: methods[0]!.channel ?? undefined,
  });
  sf.checkout.redirect({ redirectUrl });                   // gateway, or the status page for free orders
} catch (error) {
  if (ItemShopError.is(error) && error.code === "LEGAL_DOCUMENTS_CHANGED") reloadTerms();
  else throw error;
}

// back from the gateway: the platform adds ?itemshop_order=<number>&itemshop_key=<key> to your returnUrl
const order = sf.checkout.returnedOrder();                 // { number, key } or null
if (order) {
  const status = await sf.orders.statusByNumber(order.number, order.key); // { status, items[], delivery, … }
  if (status.delivery?.ready) {
    const { items } = await sf.orders.delivery(order.number, order.key);  // keys, text, download link
  }
}

// voucher redemption — creates a free order; not retried (one-time code)
await sf.vouchers.redeem("ABC-123", "Steve");

// widgets — empty when the owner made the data private
await sf.widgets.recentPurchases({ limit: 5 });
await sf.widgets.topSpenders({ limit: 10 });
await sf.widgets.monthlyGoal();                            // { enabled, percent }
await sf.widgets.onlinePlayers();                          // { count, players: string[] }
```

The site never touches payment data — `checkout.create` returns a `redirectUrl` to the gateway. If you omit `consents.legalVersion` the SDK sends the current `shop.legal.version`; pass the version you actually showed the buyer so a stale page gets `LEGAL_DOCUMENTS_CHANGED`. After `CAPTCHA_REQUIRED`, retry checkout with `captchaToken`. Types (`StorefrontShop`, `StorefrontProduct`, `CheckoutResult`…) are generated from the [Storefront API](https://andcze.mintlify.site/en/developers/storefront-api.md) spec.

## React

```tsx theme={null}
import { StorefrontProvider, useShop, useProducts, useCart, useCheckout } from "@itemshop/sdk/react";

export function App() {
  return (
    <StorefrontProvider /* options={{ shopId }} initialData={snapshot} */>
      <Shop />
    </StorefrontProvider>
  );
}

function Shop() {
  const { data: shop } = useShop();
  const { products, loading, error } = useProducts();
  const cart = useCart();                            // persisted in localStorage, synced across tabs
  const { checkout, loading: paying, error: checkoutError } = useCheckout();

  if (loading) return <Spinner />;
  if (error) return <p>{error.message}</p>;
  return (
    <>
      <h1>{shop?.displayName}</h1>
      {products.map((p) => (
        <button key={p.id} onClick={() => cart.add(p)}>{p.name} — {p.price} {p.currency}</button>
      ))}
      <p>{cart.count} items, {cart.subtotal} {shop?.currency}</p>
      <button
        disabled={paying || cart.isEmpty}
        onClick={() => checkout({ nick, consents: { terms: true, immediateDelivery: true }, paymentMethodCode: "imoje", paymentChannel: "blik" })}
      >
        Pay
      </button>
      {checkoutError && <p role="alert">{checkoutError.message}</p>}
    </>
  );
}
```

* Hooks share one request cache per provider. Nothing is fetched during SSR, and the cart renders empty on the server and during hydration.
* `useCart()` returns `items`, `count`, `subtotal`, `isEmpty`, `add(product, qty, variant)`, `remove(productId, variant?)` and `clear()`. The cart holds up to 20 lines and `checkout()` sends them all as one payment group.
* `useCheckout()` takes the cart's lines, sends one `Idempotency-Key` per attempt (a double click never creates two orders), redirects to the gateway (`redirect: false` opts out) and exposes `validateDiscount(code)`.
* Also: `useRedeemVoucher()`, `useProduct(slug)`, `usePaymentMethods()`, `useOrderStatusByNumber({ number, key })`, `useRecentPurchases()`, `useTopSpenders()`, `useMonthlyGoal()`, `useOnlinePlayers()`, `useStorefront()` (the raw client) and `useStorefrontQuery(key, fn)`.
* `initialData` accepts the build-time snapshot (`await sf.snapshot()` reads `/itemshop.data.json`).

## API client (with a key)

Server-side only — `createClient` refuses to start in a browser (the key would be public) unless you pass `dangerouslyAllowBrowser: true`. Key permissions: [API with a key](https://andcze.mintlify.site/en/developers/api-keys.md).

```ts theme={null}
import { createClient } from "@itemshop/sdk/client";

const api = createClient({ apiKey: process.env.ITEMSHOP_API_KEY! });

const products = await api.products.list(shopId, { active: true });
await api.products.update(shopId, products[0]!._id!, { price: 24.99 });

// paginated lists: await one page, or iterate everything lazily
const page = await api.orders.list(shopId, { status: "paid", limit: 50 });
for await (const order of api.orders.list(shopId, { status: "paid" })) console.log(order.orderNumber);

// any documented endpoint, fully typed from OpenAPI (path without /api/v2)
const stats = await api.request("get", "/stats/{shopId}/dashboard", { path: { shopId }, query: { period: "30d" } });

// site: upload → build → preview → production
const revision = await api.site.upload(shopId, zipBytes, { message: "New hero" });
const build = await api.site.builds.create(shopId, revision.id);
const done = await api.site.builds.wait(shopId, build.id);
const preview = await api.site.deployments.create(shopId, done.id, "preview");
await api.site.deployments.promote(shopId, preview.id);

// digital delivery: key pool of a license_keys product
await api.digital.addKeys(shopId, productId, { keys: ["AAAA-BBBB-CCCC-DDDD"] });   // also `text` (one per line) or `csv`; duplicates skipped
const pool = await api.digital.listKeys(shopId, productId, { status: "available" }); // masked values + stats
await api.digital.resend(shopId, orderId, { renewDownload: true });                 // re-send the e-mail (5/h per order)

// AI Workspace: agent, MCP tool groups, tokens, activity
await api.workspace.update(shopId, { agentId: "claude-code", mcp: { toolGroups: { orders: "read" } } });
const { token } = await api.workspace.tokens.create(shopId, { name: "Claude Code", scopes: ["site:read", "site:write"] }); // shown once
const setup = await api.workspace.agentSetup(shopId, "codex"); // { steps, mcpConfig, instructions, … }
```

The client covers, among others, `products`, `orders`, `stats`, `apiKeys`, `templates`, `site` (`files`, `draft`, `revisions`, `upload`, `builds`, `deployments`, `ai`), `workspace`, `agents`, `skills`, `scopes`.

## Errors, retries, cancellation

Every failure is an `ItemShopError`:

```ts theme={null}
error.code    // "LEGAL_CONSENT_REQUIRED" (from the backend) | "NOT_FOUND" | "RATE_LIMITED" | "NETWORK_ERROR" | "TIMEOUT" | "ABORTED" | …
error.status  // HTTP status, 0 when there was no response
error.message // human text — branch on `code`, not on this
error.details, error.retryAfter, error.body, error.isRetryable, error.isAbort
```

* **Retries**: GET/HEAD and requests carrying an `Idempotency-Key` retry network errors, timeouts and 408/425/429/5xx (2 retries, exponential backoff with jitter, `Retry-After` honoured up to 5 s). Any other request retries only on 429. Configure with `retry: { retries, baseDelayMs, maxDelayMs }` or `retry: false`.
* **Idempotency**: `checkout.create` sends a fresh key automatically; pass `idempotencyKey` to control it.
* **Timeouts**: 30 s per attempt (`timeoutMs`), uploads 5 min.
* **Cancellation**: every method takes `{ signal }` as its last argument.

```ts theme={null}
const controller = new AbortController();
sf.products.list({}, { signal: controller.signal });
controller.abort(); // rejects with code "ABORTED"
```

Options shared by both factories: `apiBase` (default `https://itemshop.dev/api/v2`), `fetch`, `headers`, `timeoutMs`, `retry`. Error codes and rate limits: [Errors and limits](https://andcze.mintlify.site/en/developers/errors-and-limits.md).

## Webhooks

ItemShop sends no signed outgoing webhooks — see [Webhooks](https://andcze.mintlify.site/en/developers/webhooks.md) (Discord) and the [plugin protocol](https://andcze.mintlify.site/en/developers/plugin-protocol.md) for real-time order delivery.

## CLI

```bash theme={null}
npx itemshop login                         # API key → ~/.config/itemshop/config.json (0600)
npx itemshop init my-shop --template bento # from itemshop-templates
cd my-shop && npm install
npx itemshop dev                           # the project's Vite with window.__ITEMSHOP__
npx itemshop deploy                        # zip → upload → build → preview URL
npx itemshop deploy --prod                 # … and promote to production
npx itemshop pull ./backup --rev draft     # download the site's files
npx itemshop skills add itemshop-theme     # agent skill
npx itemshop mcp config --agent codex      # MCP config: claude-code | codex | opencode | cursor | gemini
```

Full reference: [CLI](https://andcze.mintlify.site/en/site/cli.md). Running your own Vite? Add the plugin instead of `itemshop dev`:

```ts vite.config.ts theme={null}
import { itemshop } from "@itemshop/sdk/vite";
export default defineConfig({ plugins: [react(), itemshop({ shopId: "…" })] }); // or ITEMSHOP_SHOP_ID
```

> **[CLI](https://andcze.mintlify.site/en/site/cli.md)**
>
> `init`, `dev`, `deploy`, `pull`.

> **[Storefront API](https://andcze.mintlify.site/en/developers/storefront-api.md)**
>
> The endpoints under the SDK.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.