@crystallize/js-api-client

中文文档
7.6.0发布于 2026/10/30MITTypeScriptESM + CJS构建来源已验证近 30 天下载 5526

Helpers and typed utilities for working with the Crystallize APIs.

原始使用文档
文档目录27 节
  1. Installation
  2. Quick start
  3. Quick summary
  4. Options and environment
  5. Available API callers
  6. Authentication overview
  7. Error handling
  8. Profiling requests
  9. GraphQL builder: json-to-graphql-query
  10. High-level helpers
  11. Catalogue Fetcher
  12. Navigation Fetcher
  13. Product Hydrater
  14. Order Fetcher
  15. Order Manager
  16. Customer and Customer Group Managers
  17. Subscription Contract Manager
  18. Cart Manager (Shop API)
  19. Shop API orders
  20. Shop API customers and locks
  21. Signature verification
  22. Plugin payload decryption
  23. Pricing utilities
  24. Binary file manager
  25. Mass Call Client (Deprecated)
  26. Recommended alternative using p-limit
  27. Legacy usage

JS API Client

Helpers and typed utilities for working with the Crystallize APIs.

v5 is a major revamp: simpler client, typed inputs via @crystallize/schema, and focused managers for common tasks (catalogue, navigation, hydration, orders, customers, subscriptions, and cart).

Installation

pnpm add @crystallize/js-api-client
# or
npm install @crystallize/js-api-client
# or
yarn add @crystallize/js-api-client

Quick start

import { createClient } from '@crystallize/js-api-client';

const api = createClient({
    tenantIdentifier: 'furniture',
    // For protected APIs, provide credentials
    // accessTokenId: '…',
    // accessTokenSecret: '…',
    // staticAuthToken: '…',
    // and more
});

// Call any GraphQL you already have (string query + variables)
const { catalogue } = await api.catalogueApi(
    `query Q($path: String!, $language: String!) {
    catalogue(path: $path, language: $language) { name path }
}`,
    { path: '/shop', language: 'en' },
);

// Don't forget to close when using HTTP/2 option (see below)
api.close();

Quick summary

  • One client with callers: catalogueApi, discoveryApi, pimApi, nextPimApi, meApi, and the Shop API callers shopCartApi, shopOrderApi, shopCustomerApi, shopLockApi, shopSubscriptionContractApi, shopBookingApi
  • High-level helpers: createCatalogueFetcher, createNavigationFetcher, createProductHydrater, createOrderFetcher, createOrderManager, createCustomerManager, createCustomerGroupManager, createSubscriptionContractManager, createCartManager, createShopOrderManager, createShopCustomerManager, createShopLock
  • Utilities: createSignatureVerifier, createPluginPayloadDecrypter, createBinaryFileManager, pricesForUsageOnTier, request profiling
  • Build GraphQL with objects using json-to-graphql-query (see section below)
  • Strong typing via @crystallize/schema inputs and outputs
  • Upgrading? See UPGRADE.md for v4 → v5 migration

Options and environment

createClient(configuration, options?)

  • configuration
    • tenantIdentifier (required)
    • tenantId optional
    • accessTokenId / accessTokenSecret or sessionId
    • bearerToken for backend-issued Bearer tokens (sent as Authorization: Bearer …)
    • staticAuthToken for read-only catalogue/discovery
    • shopApiToken optional; otherwise auto-fetched
    • shopApiStaging to use the staging Shop API
    • origin custom host suffix (defaults to .crystallize.com)
  • options
    • useHttp2 enable HTTP/2 transport
    • timeout request timeout in milliseconds; requests that take longer will be aborted
    • http2IdleTimeout HTTP/2 idle timeout in milliseconds (default 300000 — 5 minutes). Use a shorter value for serverless functions, a longer one for long-running servers
    • profiling callbacks
    • extraHeaders extra request headers for all calls
    • shopApiToken controls auto-fetch: { doNotFetch?: boolean; scopes?: string[]; expiresIn?: number }. scopes defaults to ['cart']; pass every endpoint you use (e.g. ['cart', 'order']) to get a single token up front, otherwise the token is refetched with the extra scope the first time another Shop API endpoint is called

client.close() should be called when you enable HTTP/2 to gracefully close the underlying session.

Available API callers

  • catalogueApi – Catalogue GraphQL
  • discoveryApi – Discovery GraphQL (replaces the old Search API)
  • pimApi – PIM GraphQL (classic /graphql soon legacy)
  • nextPimApi – PIM Next GraphQL (scoped to tenant)
  • meApi – Me GraphQL (/@me, authenticated-user scoped)
  • shopCartApi – Shop API /cart GraphQL (token handled for you)
  • shopOrderApi, shopCustomerApi, shopLockApi, shopSubscriptionContractApi, shopBookingApi – the other Shop API endpoints (/order, /customer, /lock, /subscription-contract, /booking)

All the Shop API callers of a client share one token. It is refreshed 5 minutes before it expires, and refetched with the union of the scopes when a caller needs an endpoint the cached token doesn't cover.

All callers share the same signature: <T>(query: string, variables?: Record<string, unknown>) => Promise<T>.

Authentication overview

Pass the relevant credentials to createClient:

  • staticAuthToken for catalogue/discovery read-only
  • accessTokenId + accessTokenSecret (or sessionId) for PIM/Shop operations
  • bearerToken for backend-issued tokens — sent as Authorization: Bearer …; accepted by catalogueApi, discoveryApi, pimApi, nextPimApi, and meApi. Also used automatically to fetch the Shop API token when no other credentials are provided.
  • shopApiToken optional; if omitted, a token will be fetched using your PIM credentials on the first Shop API call

Authentication priority (per caller, highest first): sessionId → bearerToken → staticAuthToken → accessTokenId/accessTokenSecret.

See the official docs for auth: https://crystallize.com/learn/developer-guides/api-overview/authentication

Error handling

API call errors throw a JSApiClientCallError with both code and statusCode properties for the HTTP status:

import { JSApiClientCallError } from '@crystallize/js-api-client';

try {
    await api.pimApi(`query { … }`);
} catch (e) {
    if (e instanceof JSApiClientCallError) {
        console.error(`HTTP ${e.statusCode}:`, e.message);
        // e.code also works (same value)
    }
}

Profiling requests

Log queries, timings and server timing if available.

import { createClient } from '@crystallize/js-api-client';

const api = createClient(
    { tenantIdentifier: 'furniture' },
    {
        profiling: {
            onRequest: (q) => console.debug('[CRYSTALLIZE] >', q),
            onRequestResolved: ({ resolutionTimeMs, serverTimeMs }, q) =>
                console.debug('[CRYSTALLIZE] <', resolutionTimeMs, 'ms (server', serverTimeMs, 'ms)'),
        },
    },
);

GraphQL builder: json-to-graphql-query

This library embraces the awesome json-to-graphql-query under the hood so you can build GraphQL queries using plain JS objects. Most helpers accept an object and transform it into a GraphQL string for you.

  • You can still call the low-level callers with raw strings.
  • For catalogue-related helpers, we expose catalogueFetcherGraphqlBuilder to compose reusable fragments.

Example object → query string:

import { jsonToGraphQLQuery } from 'json-to-graphql-query';

const query = jsonToGraphQLQuery({
    query: {
        catalogue: {
            __args: { path: '/shop', language: 'en' },
            name: true,
            path: true,
        },
    },
});

High-level helpers

These helpers build queries, validate inputs using @crystallize/schema, and call the correct API for you.

Catalogue Fetcher

import { createCatalogueFetcher, catalogueFetcherGraphqlBuilder as b } from '@crystallize/js-api-client';

const fetchCatalogue = createCatalogueFetcher(api);

const data = await fetchCatalogue<{ catalogue: { name: string; path: string } }>({
    catalogue: {
        __args: { path: '/shop', language: 'en' },
        name: true,
        path: true,
        ...b.onProduct({}, { onVariant: { sku: true, name: true } }),
    },
});

Navigation Fetcher

import { createNavigationFetcher } from '@crystallize/js-api-client';

const nav = createNavigationFetcher(api);
const tree = await nav.byFolders('/', 'en', 3, /* extra root-level query */ undefined, (level) => {
    if (level === 1) return { shape: { identifier: true } };
    return {};
});

Product Hydrater

Fetch product/variant data by paths or SKUs with optional price contexts.

import { createProductHydrater } from '@crystallize/js-api-client';

const hydrater = createProductHydrater(api, {
    marketIdentifiers: ['eu'],
    priceList: 'b2b',
    priceForEveryone: true,
});

const products = await hydrater.bySkus(
    ['SKU-1', 'SKU-2'],
    'en',
    /* extraQuery */ undefined,
    (sku) => ({ vatType: { name: true, percent: true } }),
    () => ({ priceVariants: { identifier: true, price: true } }),
);

Order Fetcher

import { createOrderFetcher } from '@crystallize/js-api-client';

const orders = createOrderFetcher(api);
const order = await orders.byId('order-id', {
    onOrder: { payment: { provider: true } },
    onOrderItem: { subscription: { status: true } },
    onCustomer: { email: true },
});

const list = await orders.byCustomerIdentifier('customer-123', { first: 20 });

Typed example (TypeScript generics):

type OrderExtras = { payment: { provider: string }[] };
type OrderItemExtras = { subscription?: { status?: string } };
type CustomerExtras = { email?: string };

const typedOrder = await orders.byId<OrderExtras, OrderItemExtras, CustomerExtras>('order-id', {
    onOrder: { payment: { provider: true } },
    onOrderItem: { subscription: { status: true } },
    onCustomer: { email: true },
});

typedOrder.payment; // typed as array with provider
typedOrder.cart[0].subscription?.status; // typed
typedOrder.customer.email; // typed

Order Manager

Create/update orders, set payments or move to pipeline stage. Inputs are validated against @crystallize/schema.

import { createOrderManager } from '@crystallize/js-api-client';

const om = createOrderManager(api);

// Register (minimal example)
const confirmation = await om.register({
    cart: [{ sku: 'SKU-1', name: 'Product', quantity: 1, price: { gross: 100, net: 80, currency: 'USD' } }],
    customer: { identifier: 'customer-123' },
});

// Update payments only
await om.setPayments('order-id', [
    {
        provider: 'STRIPE',
        amount: { gross: 100, net: 80, currency: 'USD' },
        method: 'card',
    },
]);

// Put in pipeline stage
await om.putInPipelineStage({ id: 'order-id', pipelineId: 'pipeline', stageId: 'stage' });

Customer and Customer Group Managers

import { createCustomerManager, createCustomerGroupManager } from '@crystallize/js-api-client';

const customers = createCustomerManager(api);
await customers.create({ identifier: 'cust-1', email: '[email protected]' });
await customers.update({ identifier: 'cust-1', firstName: 'John' });

const groups = createCustomerGroupManager(api);
await groups.create({ identifier: 'vip', name: 'VIP' });

Subscription Contract Manager

Create/update contracts and generate a pre-filled template from a variant.

import { createSubscriptionContractManager } from '@crystallize/js-api-client';

const scm = createSubscriptionContractManager(api);

const template = await scm.createTemplateBasedOnVariantIdentity(
    '/shop/my-product',
    'SKU-1',
    'plan-identifier',
    'period-id',
    'default',
    'en',
);

// …tweak template and create
const created = await scm.create({
    customerIdentifier: 'customer-123',
    tenantId: 'tenant-id',
    payment: {
        /* … */
    },
    ...template,
});

Cart Manager (Shop API)

Token handling is automatic (unless you pass shopApiToken and set shopApiToken.doNotFetch: true).

import { createCartManager } from '@crystallize/js-api-client';

const cart = createCartManager(api);

// Hydrate a cart from input
const hydrated = await cart.hydrate({
    language: 'en',
    items: [{ sku: 'SKU-1', quantity: 1 }],
});

// Add/remove items, abandon or place and fulfill the cart and assign the orderId
await cart.addSkuItem(hydrated.id, { sku: 'SKU-2', quantity: 2 });
await cart.setCustomer(hydrated.id, { identifier: 'customer-123', email: '[email protected]' });
await cart.setMeta(hydrated.id, { merge: true, meta: [{ key: 'source', value: 'web' }] });
await cart.abandon(hydrated.id);
await cart.place(hydrated.id);
await cart.fulfill(hydrated.id, orderId);

Shop API orders

createShopOrderManager talks to the Shop API /order endpoint: turn a cart into an order at checkout, record payments and move the order through pipelines. (createOrderManager above targets the Core API.) Inputs are validated with the schemas from @crystallize/schema/shop, and every method returns the order id plus the fields you select.

import { createShopOrderManager } from '@crystallize/js-api-client';
import type { Order } from '@crystallize/schema/shop';

const orders = createShopOrderManager(api);

const order = await orders.createFromCart(cartId, {
    type: 'standard',
    paymentStatus: 'paid',
    payments: [{ provider: 'stripe', transactionId: 'pi_123', amount: 100, method: 'card' }],
    pipelines: [{ identifier: 'fulfilment', stage: 'new' }],
});

await orders.addPayments(order.id, [{ provider: 'gift-card', amount: 20, meta: [{ key: 'code', value: 'XMAS' }] }]);
await orders.setPayments(order.id, [{ provider: 'stripe', amount: 120 }]); // replaces all payments
await orders.setMeta(order.id, { meta: [{ key: 'source', value: 'web' }] }); // merge: true by default
await orders.setCustomer(order.id, { isGuest: false, identifier: 'customer-123', type: 'individual', addresses: [] });
await orders.addToStage(order.id, 'fulfilment', 'shipped');
await orders.removeFromPipeline(order.id, 'fulfilment');

const fetched = await orders.fetch<Order>(order.id, { reference: true, payments: { provider: true, meta: true } });
const history = await orders.listByCustomer('customer-123', { limit: 10, skip: 0 }, { reference: true });
  • payments accept any provider string: they are a record of what happened, nothing is charged.
  • payments[].meta is written as a [{ key, value }] list but read back as a JSON object ({ code: 'XMAS' }).
  • createFromCart, addPayments and setPayments persist the order just after responding: a read issued immediately after can still return the previous state.
  • fetch rejects with a JSApiClientCallError when the order does not exist.

Shop API customers and locks

import { createShopCustomerManager, createShopLock } from '@crystallize/js-api-client';

// `/customer`: customers keyed by identifier
const customers = createShopCustomerManager(api);
await customers.upsert({ identifier: 'customer-123', email: '[email protected]', firstName: 'John' }); // create or update
await customers.addAddress('customer-123', { type: 'delivery', street: 'Main St 1', city: 'Oslo' });
await customers.setAddress('customer-123', 0, { type: 'delivery', street: 'Main St 2', city: 'Oslo' });
await customers.removeAddress('customer-123', 0);
await customers.setMeta('customer-123', { meta: [{ key: 'tier', value: 'gold' }] });
const customer = await customers.fetch('customer-123', { email: true, addresses: { city: true } });

// `/lock`: a distributed lock, e.g. so a webhook and a redirect don't both create the order
const lock = createShopLock(api);
if (await lock.acquire(`cart-${cartId}`, 30)) {
    // ttl in seconds, defaults to 60; resolves false when the lock is already held
    try {
        // …createFromCart
    } finally {
        await lock.release(`cart-${cartId}`);
    }
}

Signature verification

Use createSignatureVerifier to validate Crystallize signatures for webhooks, apps or frontend calls. The verifier decodes the HS256 JWT envelope with the shared secret and matches its hmac claim against a SHA-256 of the reconstructed challenge — all through the bundled jose and the platform's crypto.subtle, so you don't need to pass your own JWT or hashing implementation.

import { createSignatureVerifier } from '@crystallize/js-api-client';

const verify = createSignatureVerifier({ secret: process.env.CRYSTALLIZE_SIGNATURE_SECRET! });

// POST example
await verify(signatureJwtFromHeader, {
    url: request.url,
    method: 'POST',
    body: rawBodyString, // IMPORTANT: raw body
});

// GET webhook example (must pass the original webhook URL)
await verify(signatureJwtFromHeader, {
    url: request.url, // the received URL including query params
    method: 'GET',
    webhookUrl: 'https://example.com/api/webhook', // the configured webhook URL in Crystallize
});

Plugin payload decryption

Use createPluginPayloadDecrypter to decrypt a Crystallize plugin JWE payload (outer JWE → nested JWS envelope → per-field encryptedSecrets) and optionally verify the inner JWS against a JWKS. This is the single entry point the CLI and any vendor-side integration should rely on.

The factory takes the vendor's private JWK (as produced by crystallize plugin keygen) and optional verify settings, and returns a reusable function that accepts a JWE compact payload per call. The private key and the JWKS resolver are built once and reused — so for a server handling many webhook calls, create the decrypter once at boot.

Only RSA-OAEP / RSA-OAEP-256 with A*GCM content encryption are accepted on the outer JWE. When the outer header carries cty: "JWT", the plaintext is treated as a compact JWS whose claims form the envelope.

Signature verification is opt-in: pass verify to enable it. When verify is omitted — or when verification fails — the envelope and per-field secrets are still returned so the caller can inspect them; signature.verified / signature.skipped / signature.reason tell you whether to trust the result.

import { readFile } from 'node:fs/promises';
import { createPluginPayloadDecrypter } from '@crystallize/js-api-client';

const privateJwk = JSON.parse(await readFile('./private.jwk.json', 'utf8'));

// Decrypt only — no signature check. Good for local dev / smoke tests.
const decrypt = createPluginPayloadDecrypter({ privateJwk });
const decoded = await decrypt(jweCompact);

if (decoded.envelope) {
    console.log('tenant:', decoded.envelope.tenantIdentifier);
    console.log('config:', decoded.envelope.config);
    console.log('secrets:', decoded.secrets); // { StripeApiKey: 'sk_live_…', … }
}

Enable verification by passing verify with at least an audience (your plugin identifier). issuer defaults to https://api.crystallize.com and jwksUrl defaults to ${issuer}/.well-known/jwks.json, so production usage is a one-liner:

// Production — issuer + JWKS URL default to api.crystallize.com.
const decrypt = createPluginPayloadDecrypter({
    privateJwk,
    verify: { audience: 'com.vendor.plugin' },
});

const verified = await decrypt(jweCompact);
if (!verified.signature.verified) {
    // Signature check skipped or failed — envelope + secrets are still populated but MUST be treated as untrusted.
    console.warn('signature not trusted:', verified.signature.reason);
}

Other verify fields: clockTolerance (seconds, defaults to 30), verifyBackendToken (also verify envelope.backendToken against the same JWKS, defaults to false).

The returned DecryptedPluginPayload contains:

  • protectedHeader — outer JWE protected header
  • innerProtectedHeader — inner JWS protected header, when the payload is nested
  • envelope — verified (or decoded) JWS claims, or null for a non-nested payload
  • plaintext — raw outer plaintext when the payload is not a nested JWT, otherwise null
  • secrets — plain-text per-field secrets decrypted from envelope.encryptedSecrets
  • signature — { verified, skipped?, reason?, issuer?, audience?, algorithm? }
  • backendToken — { verified, skipped?, reason?, claims? } when envelope.backendToken is present, otherwise null

Security: secrets and decoded envelope claims contain cleartext credentials. Do not log or forward them to shared sinks.

Pricing utilities

import { pricesForUsageOnTier } from '@crystallize/js-api-client';

const usage = 1200;
const total = pricesForUsageOnTier(
    usage,
    [
        { threshold: 0, price: 0, currency: 'USD' },
        { threshold: 1000, price: 0.02, currency: 'USD' },
    ],
    'graduated',
);

Binary file manager

Upload files (like images) to your tenant via pre-signed requests. Server-side only.

import { createBinaryFileManager } from '@crystallize/js-api-client';

const files = createBinaryFileManager(api);
const mediaKey = await files.uploadImage('/absolute/path/to/picture.jpg');
const staticKey = await files.uploadFile('/absolute/path/to/static/file.pdf');
const bulkKey = await files.uploadMassOperationFile('/absolute/path/to/import.zip');
// Use the returned keys in subsequent PIM mutations

uploadImage validates that the file is an image before creating a MEDIA upload. Use uploadFile for assets that should live in the tenant's static file storage, and uploadMassOperationFile for imports handled by the mass operations pipeline. Call uploadToTenant directly if you need lower-level control (e.g., custom buffers or upload types).

Mass Call Client (Deprecated)

Deprecated: Use mature ecosystem packages like p-limit or p-queue instead. They provide better error handling, TypeScript support, and are actively maintained.

import pLimit from 'p-limit';
import { createClient } from '@crystallize/js-api-client';

const api = createClient({ tenantIdentifier: 'my-tenant', accessTokenId: '…', accessTokenSecret: '…' });
const limit = pLimit(5); // max 5 concurrent requests

const mutations = items.map((item) =>
    limit(() =>
        api.pimApi(
            `mutation UpdateItem($id: ID!, $name: String!) { product { update(id: $id, input: { name: $name }) { id } } }`,
            { id: item.id, name: item.name },
        ),
    ),
);

const results = await Promise.allSettled(mutations);
const failed = results.filter((r) => r.status === 'rejected');
console.log(`Done: ${results.length - failed.length} succeeded, ${failed.length} failed`);

Legacy usage

The mass call client is still functional but will emit a deprecation warning on first use.

Sometimes, when you have many calls to do, whether they are queries or mutations, you want to be able to manage them asynchronously. This is the purpose of the Mass Call Client. It will let you be asynchronous, managing the heavy lifting of lifecycle, retry, incremental increase or decrease of the pace, etc.

These are the main features:

  • Run initialSpawn requests asynchronously in a batch. initialSpawn is the size of the batch by default
  • If there are more than 50% errors in the batch, it saves the errors and continues with a batch size of 1
  • If there are less than 50% errors in the batch, it saves the errors and continues with the current batch size minus 1
  • If there are no errors, it increments (+1) the number of requests in a batch, capped to maxSpawn
  • If the error rate is 100%, it waits based on Fibonacci increment
  • At the end of all batches, you can retry the failed requests
  • Optional lifecycle function onBatchDone (async)
  • Optional lifecycle function onFailure (sync) allowing you to do something and decide to let enqueue (return true: default) or return false and re-execute right away, or any other actions
  • Optional lifecycle function beforeRequest (sync) to execute before each request. You can return an altered request/promise
  • Optional lifecycle function afterRequest (sync) to execute after each request. You also get the result in there, if needed
const client = createMassCallClient(api, { initialSpawn: 1 });

async function run() {
    for (let i = 1; i <= 54; i++) {
        client.enqueue.catalogueApi(`query { catalogue { id, key${i}: name } }`);
    }

    const successes = await client.execute();
    console.log('First pass done ', successes);
    console.log('Failed Count: ' + client.failureCount());
    while (client.hasFailed()) {
        console.log('Retrying...');
        const newSuccesses = await client.retry();
        console.log('Retry pass done ', newSuccesses);
    }
    console.log('ALL DONE!');
}
run();

版本与发布记录

@crystallize/js-api-client 共发布 73 个版本,首次发布于 2022/03/26;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
7.6.0latest2026/10/3012 天前
7.5.02026/10/071 个月前
7.4.02026/09/231 个月前
7.3.02026/07/293 个月前
7.2.12026/06/134 个月前
7.2.02026/05/076 个月前

关于 @crystallize/js-api-client 的常见问题

@crystallize/js-api-client 是什么?

Helpers and typed utilities for working with the Crystallize APIs.

如何安装 @crystallize/js-api-client?

在项目目录运行 npm install @crystallize/js-api-client,或使用 pnpm add / yarn add / bun add。

@crystallize/js-api-client 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@crystallize/js-api-client 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@crystallize/js-api-client 可以商用吗?

@crystallize/js-api-client 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@crystallize/js-api-client 的下载量有多少?

根据 npm 官方下载统计,@crystallize/js-api-client 在 2026/10/11 – 2026/11/09 这 30 天内被下载 5,526 次,日均约 184 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@crystallize/js-api-client 还在维护吗?

最新版本 7.6.0 发布于 2026/10/30(12 天前);至今共发布 73 个版本,其中过去 12 个月发布了 11 个;首次发布于 2022/03/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。