Storefront client
@shopeazy/storefront-client: the typed client for the Storefront API.
@shopeazy/storefront-client is the typed client for the Storefront API. It has no runtime dependencies, uses fetch, and is ESM with types. It runs on Node 18 and later, in browsers and on edge runtimes.
It holds no platform credentials. Every call is HTTPS with a per-store key the merchant issued and can revoke. A site built on it can be handed to someone else by issuing one new key.
Install
The package is on npm under an MIT licence:
npm install @shopeazy/storefront-clientCreate a client
import { createStorefrontClient } from '@shopeazy/storefront-client';
const shopeazy = createStorefrontClient({ apiKey: process.env.SHOPEAZY_API_KEY! });
const store = await shopeazy.store.get();
const { items: products } = await shopeazy.products.page();The client throws if you construct it with a secret key (sez_sk_) in a browser. Use a publishable key (sez_pub_) in client code. See Storefront keys.
Options
createStorefrontClient({
apiKey,
baseUrl, // default https://api.shopeazy.co
shopperToken, // a one-hour Firebase ID token, sent on every call
shopperSession, // a 14-day shopper session, sent on every call
fetch, // your framework's fetch, or a fake in tests
timeoutMs, // default 15000
paths, // 'current' (default) or 'planned'
});| Option | Meaning |
|---|---|
baseUrl | https://api.shopeazy.co by default. |
shopperToken, shopperSession | A shopper credential on every call. See Shoppers |
timeoutMs | How long to wait before the call fails with timeout |
paths | Which paths to call. planned calls the /v1/store/... paths. current, the default, calls the paths the API answered on before the move |
The paths option
The Storefront API lives under /v1/store. The client calls the earlier paths by default, and paths: 'planned' switches to /v1/store/.... When the move is complete, a client release flips the default, and your code does not change. Until you upgrade, set paths: 'planned' to match these docs. See Versioning and deprecation.
What you can call
// Catalogue
await shopeazy.store.get(); // membership gate, auth.tenantId, payments.canCharge
await shopeazy.products.page({ kind: 'service' }); // physical by default. 'all' for everything
await shopeazy.products.get(slug);
await shopeazy.products.availability(slug, { from, to });
await shopeazy.services.page();
await shopeazy.kits.page();
// Policies. The slug for refunds is `refunds`, plural
await shopeazy.policies.page();
await shopeazy.policies.get('refunds'); // .html is sanitised by the API
// Selling
await shopeazy.checkout.quote({ lines, fulfilment: 'delivery', shippingAddress });
const placed = await shopeazy.orders.create({ lines, customer: { name } });
await shopeazy.orders.get(id, { orderToken: placed.accessToken });
await shopeazy.orders.pay(id, { successUrl, cancelUrl });
await shopeazy.orders.confirm(id, { orderToken: placed.accessToken });
// Contact form
await shopeazy.enquiries.submit({ name, phone, notes });
// A signed-in shopper
await shopeazy.shopper.createSession(idToken); // 14-day session
await shopeazy.asShopperSession(token).me();
await shopeazy.asShopperSession(token).members.apply({ applicationNote });
await shopeazy.asShopperSession(token).account.ordersPage();
await shopeazy.asShopperSession(token).account.bookingsPage();
// Merchant side. Secret key only
await shopeazy.members.page({ status: 'pending' });
await shopeazy.members.approve(uid, { note: 'Welcome' });Every input and result type is exported, for example Store, Product, Quote, Order and Me. They come from the specification.
Lists
Each list has a page() method that takes { limit, cursor } and returns { items, nextCursor }. Pass nextCursor back as cursor until it is null, or use listAll:
import { listAll } from '@shopeazy/storefront-client';
const everything = await listAll((cursor) => shopeazy.products.page({ cursor }));The older list() methods still work but are deprecated, as are the API fields they read. They go after 1 February 2027. Use page().
Idempotency
orders.create and orders.pay send a fresh Idempotency-Key on every call. To make your own retry safe, make one key per attempt and pass it each time:
await shopeazy.orders.create(input, { idempotencyKey });A retry returns the first order instead of placing a second. The same key with a different body is 409 idempotency_key_reused. See Idempotency.
Keeping a shopper signed in
import { shopperCookie, readShopperToken, clearShopperCookie } from '@shopeazy/storefront-client/session';
const { sessionToken } = await shopeazy.shopper.createSession(idToken);
headers.set('Set-Cookie', shopperCookie(sessionToken, { maxAgeSeconds: 60 * 60 * 24 * 14 }));
const token = readShopperToken(request.headers.get('cookie'));
const me = token ? await shopeazy.asShopperSession(token).me() : null;peekTokenClaims() and isTokenExpired() read a token without verifying it. They are advisory: never authorise on them.
Helpers
Pure functions, also at @shopeazy/storefront-client/helpers:
| Helper | Does |
|---|---|
formatMoney(money) | { amount: 123450, currency: 'ZAR' } to R1 234,50 |
displayPrice(product) | The card price: the cheapest priced variant, else the item price |
membersOnly(store.membership) | true when the store sells to members only |
membershipGate(store.membership) | 'checkout' or 'browse' |
Errors
import { StorefrontApiError } from '@shopeazy/storefront-client';
try {
await shopeazy.orders.create(input);
} catch (error) {
if (error instanceof StorefrontApiError) {
if (error.isShopperTokenExpired) return refreshAndRetry();
if (error.needsShopper) return showSignIn();
if (error.isNotMember) return showApply();
if (error.isStoreClosed) return showShopClosed();
if (error.isAccessRevoked) return tellSomeoneTheKeyIsOff();
console.error(error.status, error.code, error.message, error.requestId);
}
}code is typed from the specification, plus three codes the client raises itself: timeout, network_error and bad_response. Branch on code, never on message. requestId and details are on the error.
Versions
The package version is the Storefront API contract's version, release part only. A new operation or optional field is a minor release. A breaking change waits for a new major version of the API and a 2.0.0 client. See Versioning and deprecation.
Email the customer a counter receipt POST
A walk-in only gets a receipt when asked, so this is that ask: it goes to the email on the order, for a customer of any kind. Once per order: a repeat, or a retry, sends nothing and answers `already_sent`. The order must have an email (422 `validation_error`: "No email for this customer.") and be paid (422: a receipt says the money arrived). A send that failed is 500 `internal_error`; try again. The mail is sent from the store's name; it is transactional, so it does not count against the daily email limit of connected AI apps. Access: OAuth scope `orders:write`; write-gated by the subscription.
Merchant client
@shopeazy/merchant-client: the typed client for the Merchant API.