npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@minipim/sdk

v0.13.0

Published

Typed TypeScript client for the MiniPim API.

Readme

@minipim/sdk

Typed TypeScript client for the MiniPim API. Generated from the OpenAPI spec at /docs/json and wrapped with openapi-fetch plus a webhook-signature helper.

Install

pnpm add @minipim/sdk

Requires Node 18+ (for native fetch) or a fetch polyfill. Ships both ESM and CommonJS builds (v0.3.0+) — import and require both work, including in CJS test tooling. The root entry imports no node: modules, so createMinipimClient and the attribute/pagination helpers are safe in Edge runtimes.

baseUrl is the origin (https://api.minipim.com); every endpoint path already includes /v1/..., so you pass pim.GET('/v1/products'), not a pre-joined URL.

Quick start

import { createMinipimClient } from '@minipim/sdk';

const pim = createMinipimClient({
  baseUrl: 'https://api.minipim.com',
  organizationId: '00000000-0000-0000-0000-000000000001',
  apiKey: process.env.MINIPIM_API_KEY!,
});

// List products
const { data, error } = await pim.GET('/v1/products', {
  params: { query: { limit: 50, sortBy: 'name', sortDir: 'asc' } },
});
if (error) throw new Error(error.error.message);
console.log(`got ${data.data.length} products, hasMore: ${data.hasMore}`);

// Single product with locale + channel resolution
const product = await pim.GET('/v1/products/{id}', {
  params: {
    path: { id: '0b8c5d3a-…' },
    query: { locale: 'en_US', channel: 'headless-main' },
  },
});
console.log(product.data?.resolvedAttributes);

Authentication

API keys (recommended for service-to-service):

const pim = createMinipimClient({
  baseUrl: 'https://api.minipim.com',
  organizationId: '<your-org-uuid>',
  apiKey: 'pim_xxxxxxxxxxxx',
});

Issue keys in the admin UI at /api-keys. x-organization-id is required separately — API keys identify the principal, not the tenant.

Dev mode (header auth) — only when the deployment is started with PIM_AUTH=header:

const pim = createMinipimClient({
  baseUrl: 'http://localhost:4100',
  organizationId: '00000000-0000-0000-0000-000000000001',
  userId: 'dev-test',
});

Webhooks

The headless connector POSTs events to your URL with an HMAC-SHA256 signature in X-MiniPim-Signature-256. Verify against the raw body — re-stringified JSON breaks the signature.

Next.js App Router

import { verifyMinipimWebhook } from '@minipim/sdk/webhook';

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get('x-minipim-signature-256') ?? '';
  // Node verifier is synchronous — no await.
  const ok = verifyMinipimWebhook({
    rawBody: raw,
    signature: sig,
    secret: process.env.MINIPIM_WEBHOOK_SECRET!,
  });
  if (!ok) return new Response('invalid signature', { status: 401 });

  const event = JSON.parse(raw);
  // event.name = 'product.updated' | 'category.deleted' | ...
  // event.entity_id, event.payload, etc.
  // Handle + return 2xx — non-2xx is silently dropped in v1 (no retries yet).
  return Response.json({ ok: true });
}

Edge / Cloudflare Workers / Vercel Edge

import { verifyMinipimWebhookEdge } from '@minipim/sdk/webhook-edge';

const ok = await verifyMinipimWebhookEdge({ rawBody, signature, secret });
if (!ok) return new Response('invalid signature', { status: 401 });

Both functions:

  • Constant-time comparison (safe against timing attacks).
  • Return false on malformed signatures (don't throw).

Node verifier is synchronous; Edge verifier is async. The Node one (@minipim/sdk/webhook) returns boolean — use it directly in if (!verifyMinipimWebhook(...)). The Edge one (@minipim/sdk/webhook-edge) returns Promise<boolean> — you must await it. They live on separate entry points so the Node node:crypto import never lands in an Edge bundle.

Pagination

Don't hand-roll the hasMore loop — the SDK ships paginate, a generic async iterator over any list endpoint:

import { paginate, collectAll } from '@minipim/sdk';
import type { components } from '@minipim/sdk';

type Product = components['schemas']['ProductSelect'];

// stream
for await (const p of paginate<Product>(pim, '/v1/products', { query: { status: 'active' } })) {
  await ingest(p);
}

// or collect everything
const all = await collectAll<Product>(pim, '/v1/products');

paginate manages limit/offset, honors the server's hasMore, and defaults to the 200-row max page size (override with pageSize).

withTotal: true is supported by /v1/products only — it adds one COUNT(*), so leave it off on hot reads. Other list endpoints never return total, and as of v0.7.0 their types say so rather than declaring a field that was always undefined.

Against API 0.14.0 and newer, hasMore is present and exact on every list endpoint — the server reads one row past the page, so a final page that happens to be exactly full reports false instead of sending you after an empty one. Older deployments omitted hasMore everywhere except /v1/products; paginate keeps a short-page fallback for those, which is the whole reason not to hand-roll the loop.

Endpoints that return a plain array instead of the envelope (/v1/categories without ?limit=, /v1/attributes, /v1/products/{id}/variants) are handled too (v0.3.0+): the array is treated as the one-and-only page, so collectAll works uniformly across every list endpoint.

Listing by category, and by price (v0.5.0+)

Two things storefronts almost always need, and previously had to do in memory.

Category subtrees. categoryId on its own is self-only — it matches products filed directly against that category. Catalogs commonly assign products to leaf categories, so a parent category can legitimately return an empty page. Add includeDescendants for the whole subtree:

// Everything anywhere under "Backdrops", in ONE paginated query.
const { data } = await pim.GET('/v1/products', {
  query: { categoryId, includeDescendants: true, limit: 50 },
});

Don't fetch /v1/categories, resolve descendants yourself and issue one request per id — this is a single round trip and it paginates as one result set.

Sorting and filtering by price. There is no sortBy: 'price', because a catalog's price attribute code is its own data (price, msrp, list_price, …). Name it:

// Cheapest first
await pim.GET('/v1/products', {
  query: { sortBy: 'attribute', sortAttribute: 'price', sortDir: 'asc' },
});

// $10.00–$50.00 — money bounds are INTEGER CENTS
await pim.GET('/v1/products', {
  query: { filterAttribute: 'price', filterMin: 1000, filterMax: 5000 },
});
  • Works on money, number and decimal attributes. measurement is refused — its values carry a unit, so ordering raw amounts would rank 5 g above 2 kg.
  • For money, bounds and ordering use amount_cents. filterMin: 1000 is $10.00. No currency conversion — mixed-currency catalogs compare by number.
  • The value read is the one at the default scope (no locale, no channel). Products with no value sort last in both directions and are excluded by either bound: no price, not a price of zero.
  • An unknown code, or one of a non-orderable type, is a 422 naming the problem. It never degrades to name ordering, so you can't get a plausible page that isn't sorted the way you asked.
  • sortAttribute and filterAttribute are independent; set both for "cheapest first, within a budget".

Needs API 0.9.0+. Check GET /healthz.

Tags

Tags are canonicalized on write, so what you read back is not verbatim what you sent:

await pim.POST('/v1/content', { body: { /* … */ tags: ['Buying Guide', 'buying guide', 'FAQ!'] } });
// stored, and returned as: ['buying-guide', 'faq']

Each tag is lower-cased, stripped of diacritics, has runs of non-alphanumerics collapsed to -, is trimmed of leading/trailing -, and is truncated to 60 characters. Empties are dropped and duplicates removed (first occurrence wins), so the array you get back can be shorter than the one you sent. Note that the schema's maxLength is 80: two tags differing only past character 60 collapse into one.

This is deliberate — it is what makes Featured, featured and FEATURED one tag rather than three. Filters are canonicalized identically, so you never have to pre-slugify one:

await pim.GET('/v1/content', { params: { query: { tag: 'Buying Guide' } } }); // matches buying-guide

tag is repeatable and ANDs: { tag: ['guide', 'seo'] } returns only pages carrying both. GET /v1/products/tags and GET /v1/content/tags list the vocabulary actually in use, with counts — two separate vocabularies, since an editorial tag and a merchandising tag rarely mean the same thing.

Documented in the field descriptions from API 0.14.1+, and in these types from v0.7.0.

Ordering a content archive (API 0.19.0+)

GET /v1/content defaults to updatedAt descending — the order things were last edited, which is rarely the order an archive should read in. Sort by publishedAt, which imports preserve from the source system:

const { data } = await pim.GET('/v1/content', {
  params: { query: { sortBy: 'publishedAt', sortDir: 'desc', limit: 20 } },
});

Pages with no publishedAt sort last in both directions — an undated page is unscheduled, not newest. sortBy also accepts updatedAt, createdAt and title.

categoryId behaves exactly like it does on products: self-only unless you add includeDescendants: true. Content gets filed against leaf categories too, so filtering by a parent section returns an empty page without it.

Uploading files

POST /v1/media is multipart/form-data, not JSON. The file field is named file; everything else is optional and lets you attach the upload in the same request:

const form = new FormData();
form.append('file', blob, 'sell-sheet.pdf');
form.append('entityType', 'product'); // 'product' | 'variant' | 'content_page'
form.append('entityId', productId);
form.append('role', 'technical'); // hero | gallery | thumbnail | technical | lifestyle | swatch
form.append('altText', JSON.stringify({ en_US: 'Sell sheet' })); // a JSON *string*, not an object

altText is parsed as JSON, so passing a real object rather than a string stores no alt text. Send JSON.stringify(...).

That case no longer fails silently. From API 0.21.0 a malformed altText still lets the upload succeed — it never fails the request — but the response says what it ignored:

const { data } = await pim.POST('/v1/media', { body: form as never });
if (data?.warnings?.length) console.warn(data.warnings);
// ["altText was not valid JSON and no alt text was stored; expected an object keyed by locale, …"]

warnings is absent when there is nothing to report, so a clean upload's response is unchanged.

You do not have to get the content type right. From API 0.21.0 the leading bytes are sniffed whenever you send application/octet-stream or no type at all — so a PNG whose filename lost its extension uploads as a PNG. If a specific declared type contradicts the bytes, the bytes win. SVG, CSV, plain text and the Office formats have no distinguishing magic bytes and still need an explicit type.

role and position are validated: an unrecognised value is a 422 listing the accepted ones in details.accepted, not a 500.

Requires API 0.14.0+ to appear in the spec at all — before that this endpoint published no request body, so generated clients had nothing for it.

Attribute helpers

Attribute values are unknown and keyed by (locale, channel). List responses return the raw { code: [{ locale, channel, value }] } shape (only product detail with ?locale=&channel= returns a flat resolvedAttributes). The SDK ships the flatten + coercion helpers so you don't reimplement them:

import { flattenAttributes, getAttribute, asMoney, asMeasurement, formatMoney } from '@minipim/sdk';

// resolve one code for a (locale, channel), with server-matching fallback
const desc = getAttribute(product.attributes, 'description', {
  locale: 'en_US',
  channel: 'headless-main',
});

// flatten the whole bag (client-side equivalent of resolvedAttributes, for lists)
const flat = flattenAttributes(product.attributes, { locale: 'en_US', channel: 'headless-main' });

// coerce money / measurement shapes
const price = asMoney(getAttribute(product.attributes, 'price')); // { amount_cents, currency } | null
if (price) console.log(formatMoney(price)); // "$18.99"
const weight = asMeasurement(getAttribute(product.attributes, 'weight')); // { amount, unit } | null

Also available at the @minipim/sdk/attributes subpath.

Modifier helpers

Modifiers are order-line options that don't create a SKU (engraving, add-ons, print/hardware choices). They live at GET /v1/products/{id}/modifiers and GET /v1/modifiers/{id} — product detail only flags their presence via modifierCount / hasRequiredModifiers, so a PDP with required options must fetch the list. Price deltas are in config.priceAdjusters, keyed by choice value slug; money deltas are integer cents + ISO currency (not dollars) and percentage deltas are integer basis points. These helpers resolve and price them:

import {
  getModifierChoices,
  getModifierPriceAdjuster,
  applyPriceAdjuster,
  formatPriceAdjuster,
  getAttribute,
  asMoney,
  formatMoney,
} from '@minipim/sdk';

const [mod] = await pim
  .GET('/v1/products/{id}/modifiers', { params: { path: { id } } })
  .then((r) => r.data);

// choices joined with their structured delta
for (const c of getModifierChoices(mod)) {
  // c.priceAdjuster is null for free choices (and product_list modifiers, whose
  // price lives on a referenced product — see DEVELOPERS.md)
  const suffix = c.priceAdjuster ? ` (${formatPriceAdjuster(c.priceAdjuster)})` : '';
  console.log(`${c.label}${suffix}`); // "Pair of LED Lights (+$170.00)"
}

// price a selected choice against the base price
const base = asMoney(getAttribute(product.attributes, 'price')); // { amount_cents, currency }
const chosen = getModifierPriceAdjuster(mod, 'pair_of_led_lights');
if (base && chosen) formatMoney(applyPriceAdjuster(base, chosen)); // "$869.00"

asPriceAdjuster / asModifierConfig narrow the opaque config; they reject the pre-2026-07 legacy shape (bare value in dollars) so the SDK never misreads dollars as cents. Also available at the @minipim/sdk/modifiers subpath.

Error handling

res.error is a per-path union that's awkward to narrow. Use the shared guard:

import { isMinipimError, getErrorMessage } from '@minipim/sdk';

const { data, error } = await pim.GET('/v1/products/{id}', { params: { path: { id } } });
if (error) {
  throw new Error(getErrorMessage(error, 'fetch failed')); // pulls error.error.message
}

Reconciliation after downtime

?updatedSince=<iso8601> filters the product list to rows modified after a timestamp. Pair with hasMore to walk the changeset back to current state:

async function catchUp(since: string) {
  for await (const p of allProducts({ updatedSince: since })) {
    await ingest(p);
  }
}

TypeScript reference

import type { paths, components, operations } from '@minipim/sdk';
type Product = components['schemas']['ProductSelect'];

The generated paths, components, and operations cover the full surface at /docs/json. Use them to type DTOs, request handlers, etc.

Related

  • Narrative integration guide — auth, pagination, locale/channel resolution, webhook contract, common integration shapes.
  • Swagger UI — interactive endpoint reference, try-it-out enabled.
  • OpenAPI spec — raw JSON, what this SDK is generated from.

License

Apache-2.0.