# JavaScript SDK

> @shopfast-sdk/client for browsers and servers: the storefront client, the admin client and webhook verification.

Source: https://shopfa.st/docs/headless/sdk

`@shopfast-sdk/client` is the typed client for the [Storefront API](https://shopfa.st/docs/headless/storefront-api), the REST API and
webhooks. It has no dependencies and runs in browsers, Node 20+, Bun, Deno, edge runtimes and Next.js.

```sh
npm install @shopfast-sdk/client
```

Types are generated from the same contract the API validates against, and a schema snapshot is checked on every
change to ShopFast, so an upgrade of the SDK never surprises you.

## The storefront client

```ts
import { createStorefrontClient } from '@shopfast-sdk/client';

const shop = createStorefrontClient({
  shop: 'your-store',          // your handle, or the st_ id
  key: 'sfpk_live_…',          // a publishable key in browsers; a private key (sfsk_…) on your server
});

const info = await shop.shop.get();                              // name, currency, status, SEO, links
const page = await shop.products.list({ collection: 'staff-picks', limit: 24 });
const product = await shop.products.get('nimbus-vpn');
const live = await shop.products.availability('nimbus-vpn');     // never cached
const found = await shop.search.query({ q: 'vpn' });
```

| Area | Methods |
| --- | --- |
| `shop` | `get`, `policies` |
| `pages`, `reviews` | `list`, `get` · `list` |
| `products` | `list`, `iterate` (every page), `get`, `availability`, `reviews`, `recommendations` |
| `collections`, `categories` | `list`, `get` (with a page of products) |
| `search` | `query`, `suggest` |
| `cart` | `create`, `get`, `addLines`, `updateLine`, `update`, `applyCode`, `removeCode`, `checkoutOptions`, `checkout`, `checkoutLink` |
| `orders` | `get(sessionId, access)` |
| `customer` | `requestCode`, `signIn`, `signOut`, `get`, `orders`, `library`, `cancelSubscription`, `wallet` |
| `support` | `open`, `list`, `get`, `reply` |

### Lists

Lists answer `{ items, total, nextCursor }`. Pass `nextCursor` back as `after`, or let `iterate` walk every page:

```ts
for await (const product of shop.products.iterate({ category: 'software' })) {
  console.log(product.title, product.priceMinor);
}
```

### Cart and checkout

```ts
const cart = await shop.cart.create({ lines: [{ productId: product.id, variantId: product.variants[0]?.id ?? null, quantity: 1 }] });
await shop.cart.applyCode(cart.id, 'LAUNCH10');

// Your own checkout page: what to ask for this cart, then start it and send the buyer to pay.
const options = await shop.cart.checkoutOptions(cart.id, { country: 'DE' });
try {
  const session = await shop.cart.checkout(cart.id, {
    email, name, country: 'DE', consent: true, provider: options.methods[0]?.provider ?? null, method: options.methods[0]?.method ?? null,
    returnUrl: 'https://shop.example.com/checkout/complete',
  });
  redirect(session.paid ? `/orders/${session.sessionId}` : session.payUrl);
} catch (error) {
  if (isShopFastError(error) && error.is('email-verification-required')) askForTheCode();
  else throw error;
}

// Or the hosted checkout, back to your site after paying.
const { checkoutUrl } = await shop.cart.checkoutLink(cart.id, { returnUrl: 'https://shop.example.com/checkout/complete' });
```

See [Headless checkout](https://shopfa.st/docs/headless/checkout) for your own checkout, the embedded one and the hosted one.

### A signed-in buyer

```ts
await shop.customer.requestCode('buyer@example.com');
const session = await shop.customer.signIn('buyer@example.com', '123456');
const buyer = shop.withCustomer(session.token);
const library = await buyer.customer.library();                  // keys, downloads, subscriptions
```

### On your server

With a private key, pass the buyer's address so limits and fraud checks see the buyer:

```ts
const shop = createStorefrontClient({ shop: 'your-store', key: process.env.SHOPFAST_STOREFRONT_KEY!, buyerIp: request.headers.get('x-forwarded-for') });
```

### Errors, retries and caching

- Errors throw `ShopFastError` with the [problem](https://shopfa.st/docs/guides/errors) fields: `status`, `type`, `title`, `detail`, `fields`
  and `retryAfter`. `isShopFastError(e)` narrows it.
- Reads retry on `429` and `503`, honoring `Retry-After`. Writes retry only when you pass an idempotency key.
- `cache: true` keeps shared answers with their `ETag` and revalidates them for free (`304`). Carts, accounts and orders
  are never kept.

## Next.js

```ts
import { createNextStorefrontClient } from '@shopfast-sdk/client/next';

export const shop = createNextStorefrontClient({ shop: 'your-store', key: process.env.SHOPFAST_STOREFRONT_KEY!, revalidate: false });
```

Every fetch carries `next: { tags }` for your shop, so the [`storefront.updated` webhook](https://shopfa.st/docs/headless/revalidation)
refreshes exactly what changed:

```ts
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { handleRevalidationWebhook } from '@shopfast-sdk/client/next';

export const POST = (request: Request) => handleRevalidationWebhook(request, { secret: process.env.SHOPFAST_WEBHOOK_SECRET!, revalidateTag });
```

The [Next.js starter](https://shopfa.st/docs/headless/nextjs) puts all of it together.

## Webhooks

```ts
import { verifyWebhook } from '@shopfast-sdk/client';

const event = await verifyWebhook({ secret: process.env.SHOPFAST_WEBHOOK_SECRET!, headers: request.headers, body: await request.text() });
// throws WebhookVerificationError on a bad signature or a delivery older than 5 minutes
```

Pass two secrets during a rotation. The body must be the raw text, exactly as received.

## The admin client

For your own store's REST API with an API key or an OAuth token:

```ts
import { createAdminClient } from '@shopfast-sdk/client';

const admin = createAdminClient({ apiKey: process.env.SHOPFAST_API_KEY! });
const orders = await admin.request('GET', '/v1/store/orders', { query: { tab: 'paid', limit: 20 } });
await admin.request('POST', '/v1/store/products', { body: { name: 'Launch Pack', type: 'service', priceMinor: 2500 } });
```

Every write carries an `Idempotency-Key` (a fresh one unless you pass yours), so a retry after a timeout never runs
twice. Pass `parse` (a zod schema's `parse`, say) to get your own type back.
