Store Hugs Developers

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

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

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);
    },
});