@minipim/sdk
v0.4.0
Published
Typed TypeScript client for the MiniPim API.
Maintainers
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/sdkRequires 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
falseon 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). Pass withTotal: true in query if you need total — it adds one COUNT(*), so leave it off on hot reads.
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.
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 } | nullAlso 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.
