TypeScript SDK
@storehugs/integration is the open-source TypeScript client for the Store Hugs Integration API. It has no dependencies and runs on Node 22+, browsers, Deno and Bun.
It has a client for each half of the API:
| Client | API | Key | Runs in |
|---|---|---|---|
StorefrontClient |
Storefront API: catalog, carts, checkout links, signed-in shoppers | Publishable (pk_live_…) or secret (sk_live_…) |
Browsers on the sites the key lists, or your server |
AdminClient |
Admin API: products, stock, orders, customers, marketing, reviews, quotations, keys, webhooks | Admin (sa_live_…) |
Your server only |
Both need the shop to be on the Pro or Business plan, with the Integration API turned on in the panel under Shop Settings → Integration API.
Install
npm install @storehugs/integration
The package is ESM only. With pnpm, Yarn or Bun use pnpm add, yarn add or bun add. On Deno, import npm:@storehugs/integration.
Quick start
A cart and a checkout link on your own storefront:
import { StorefrontClient } from '@storehugs/integration';
const storefront = new StorefrontClient({
host: 'acme.storehugs.com',
key: 'pk_live_…',
});
const { data: products } = await storefront.products.list({
search: 'mug',
perPage: 12,
});
const cart = await storefront.createCart();
await storefront.cart(cart.cartToken).addItem(products[0].id, 2);
const { checkoutUrl } = await storefront
.cart(cart.cartToken)
.checkout({ returnPath: '/thanks' });
// Open checkoutUrl in the shopper's browser.
Every order changed since a date, from your server:
import { AdminClient } from '@storehugs/integration';
const admin = new AdminClient({ apiKey: process.env.STOREHUGS_ADMIN_KEY! });
for await (const order of admin.orders.all({
updatedSince: '2026-11-01T00:00:00Z',
})) {
console.log(order.orderNumber, order.status);
}
Errors
A refusal throws IntegrationApiError. Network failures that outlast the retries throw the fetch error itself.
| Property | Type | What it is |
|---|---|---|
status |
number |
The HTTP status. |
code |
string | null |
The API's code, such as invalid_api_key, insufficient_scope, edit_conflict, limit_reached or idempotency_key_reused. |
errors |
Record<string, string[]> |
Messages per field, on a 422. |
isValidationError |
boolean |
Whether it's a 422 with messages per field. |
message |
string |
The API's message. |
body |
unknown |
The parsed answer. |
response |
Response |
The raw answer. |
import { IntegrationApiError } from '@storehugs/integration';
try {
await admin.products.update(
12,
{ price: 19.5 },
{ unmodifiedSince: product.updatedAt ?? undefined },
);
} catch (error) {
if (
error instanceof IntegrationApiError &&
error.code === 'edit_conflict'
) {
// Someone changed the product since you read it: read it again and retry.
} else if (
error instanceof IntegrationApiError &&
error.isValidationError
) {
console.log(error.errors); // { price: ['…'] }
} else {
throw error;
}
}
Retries and safe writes
- Every write is safe to retry. Each
POST,PUT,PATCHandDELETEgets anIdempotency-Key, kept across retries, so a retried write never runs twice. PassidempotencyKeyin a write's options to keep one across processes (for example, from a job id), orfalseto send none. - Retries: 429 and 503 answers and network errors are retried up to
maxRetriestimes (default 2). The client waits forRetry-After(at most 60 seconds), or backs off from half a second. - A write still running: when a retry reaches the server while the first attempt still runs (409
idempotency_in_progress), the client waits under the same key, for up toinProgressTimeoutMs(default 30 seconds), and returns the first attempt's answer. - Cancelling: pass an
AbortSignalassignalin a write's options.
Methods that write take these options last, as RequestOptions:
| Option | Type | |
|---|---|---|
idempotencyKey |
string | false |
Your own key, or false for none. |
signal |
AbortSignal |
Cancels the request. |
headers |
Record<string, string | undefined> |
Extra headers. |
Pagination
- Storefront API lists are numbered pages:
{ data, links, meta }, wheremetahascurrentPage,lastPage,perPageandtotal. PasspageandperPage. - Admin API lists are cursor pages:
{ data, links, meta }, wheremeta.nextCursoris the next page's cursor, ornullon the last. Each Admin resource haslist(query)for one page andall(query), an async iterator that fetches every page as you read:
for await (const customer of admin.customers.all({ limit: 100 })) {
// …
}
Webhooks
Register endpoints in the panel or with admin.webhooks.create. Each delivery is signed with your endpoint's whsec_… secret. Check it with verifyWebhookSignature, on the raw body exactly as received:
import {
verifyWebhookSignature,
WEBHOOK_HEADERS,
} from '@storehugs/integration';
export async function POST(request: Request): Promise<Response> {
const rawBody = await request.text();
const genuine = await verifyWebhookSignature(
rawBody,
request.headers.get(WEBHOOK_HEADERS.signature),
process.env.STOREHUGS_WEBHOOK_SECRET!,
);
if (!genuine) {
return new Response('Invalid signature', { status: 400 });
}
const eventId = request.headers.get(WEBHOOK_HEADERS.eventId);
// Skip an event id you've already handled: retries reuse it.
const event = JSON.parse(rawBody);
return new Response(null, { status: 204 });
}
verifyWebhookSignature(rawBody, signatureHeader, secret, options?)
Resolves to true when an HMAC-SHA256 of the delivery matches one of the header's v1 signatures (two while a replaced secret still signs) and the signed time is recent.
| Option | Default | |
|---|---|---|
toleranceSeconds |
300 |
How far the signed time may be from now. |
now |
the current time | The current time in seconds, for tests. |
WEBHOOK_HEADERS
The headers every delivery carries: eventId (Storehugs-Event-Id), eventType (Storehugs-Event-Type) and signature (Storehugs-Signature).
The Admin API reference lists every event and its data.
PKCE helpers
Shopper sign-in and app approval both use OAuth with S256 PKCE. The SDK makes the values for you:
createPkcePair()
Resolves to { verifier, challenge }. Send the challenge with the redirect, keep the verifier (in the session) for the code exchange.
createState()
Returns a random state to send with the redirect and check on the way back.
See shopper sign-in and apps.
Types
Every request and answer is typed from the OpenAPI file. Import the types you need:
import type {
Cart,
Order,
ProductDetail,
AdminProduct,
AdminApiScope,
} from '@storehugs/integration';
Custom fetch
Both clients take a fetch option, (input: string, init: RequestInit) => Promise<Response>, which defaults to the global fetch. Use it to add logging, a proxy, or a fake in your tests.
const admin = new AdminClient({
apiKey: process.env.STOREHUGS_ADMIN_KEY!,
fetch: (input, init) => {
console.log(init.method, input);
return fetch(input, init);
},
});