JS API Client
Helpers and typed utilities for working with the Crystallize APIs.
Installation
pnpm add @crystallize/js-api-client
# or
npm install @crystallize/js-api-client
# or
yarn add @crystallize/js-api-client
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();
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
catalogueApi
,discoveryApi
discoveryApi
,pimApi
pimApi
,nextPimApi
nextPimApi
shopCartApi
shopCartApi
- High-level helpers: ,
createCatalogueFetcher
createCatalogueFetcher
,createNavigationFetcher
createNavigationFetcher
,createProductHydrater
createProductHydrater
,createOrderFetcher
createOrderFetcher
,createOrderManager
createOrderManager
,createCustomerManager
createCustomerManager
,createCustomerGroupManager
createCustomerGroupManager
,createSubscriptionContractManager
createSubscriptionContractManager
createCartManager
createCartManager
- Utilities: ,
createSignatureVerifier
createSignatureVerifier
,createBinaryFileManager
createBinaryFileManager
, requestpricesForUsageOnTier
pricesForUsageOnTier
profiling
profiling
- Build GraphQL with objects using (see section below)
json-to-graphql-query
json-to-graphql-query
- Strong typing via inputs and outputs
@crystallize/schema
@crystallize/schema
- Upgrading? See UPGRADE.md for v4 → v5 migration
Options and environment
createClient(configuration, options?)
createClient(configuration, options?)
- configuration
- (required)
tenantIdentifier
tenantIdentifier
- optional
tenantId
tenantId
- /
accessTokenId
accessTokenId
oraccessTokenSecret
accessTokenSecret
sessionId
sessionId
- for read-only catalogue/discovery
staticAuthToken
staticAuthToken
- optional; otherwise auto-fetched
shopApiToken
shopApiToken
- to use the staging Shop API
shopApiStaging
shopApiStaging
- custom host suffix (defaults to
origin
origin
).crystallize.com
.crystallize.com
- options
- enable HTTP/2 transport
useHttp2
useHttp2
- callbacks
profiling
profiling
- extra request headers for all calls
extraHeaders
extraHeaders
- controls auto-fetch:
shopApiToken
shopApiToken
{ doNotFetch?: boolean; scopes?: string[]; expiresIn?: number }
{ doNotFetch?: boolean; scopes?: string[]; expiresIn?: number }
client.close()
client.close()
Available API callers
- – Catalogue GraphQL
catalogueApi
catalogueApi
- – Discovery GraphQL (replaces the old Search API)
discoveryApi
discoveryApi
- – PIM GraphQL (classic /graphql soon legacy)
pimApi
pimApi
- – PIM Next GraphQL (scoped to tenant)
nextPimApi
nextPimApi
- – Shop Cart GraphQL (token handled for you)
shopCartApi
shopCartApi
All callers share the same signature:
<T>(query: string, variables?: Record<string, unknown>) => Promise<T>
<T>(query: string, variables?: Record<string, unknown>) => Promise<T>
Authentication overview
Pass the relevant credentials to
createClient
createClient
- for catalogue/discovery read-only
staticAuthToken
staticAuthToken
- +
accessTokenId
accessTokenId
(oraccessTokenSecret
accessTokenSecret
) for PIM/Shop operationssessionId
sessionId
- optional; if omitted, a token will be fetched using your PIM credentials on first cart call
shopApiToken
shopApiToken
See the official docs for auth: https://crystallize.com/learn/developer-guides/api-overview/authentication
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)'),
},
},
);
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 to compose reusable fragments.
catalogueFetcherGraphqlBuilder
catalogueFetcherGraphqlBuilder
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,
},
},
});
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
@crystallize/schema
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 } }),
},
});
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 {};
});
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 } }),
);
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 });
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
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
@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' });
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: 'john@doe.com' });
await customers.update({ identifier: 'cust-1', firstName: 'John' });
const groups = createCustomerGroupManager(api);
await groups.create({ identifier: 'vip', name: 'VIP' });
import { createCustomerManager, createCustomerGroupManager } from '@crystallize/js-api-client';
const customers = createCustomerManager(api);
await customers.create({ identifier: 'cust-1', email: 'john@doe.com' });
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,
});
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
shopApiToken
shopApiToken.doNotFetch: true
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 and place the order
await cart.addSkuItem(hydrated.id, { sku: 'SKU-2', quantity: 2 });
await cart.setCustomer(hydrated.id, { identifier: 'customer-123', email: 'john@doe.com' });
await cart.setMeta(hydrated.id, { merge: true, meta: [{ key: 'source', value: 'web' }] });
await cart.place(hydrated.id);
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 and place the order
await cart.addSkuItem(hydrated.id, { sku: 'SKU-2', quantity: 2 });
await cart.setCustomer(hydrated.id, { identifier: 'customer-123', email: 'john@doe.com' });
await cart.setMeta(hydrated.id, { merge: true, meta: [{ key: 'source', value: 'web' }] });
await cart.place(hydrated.id);
Signature verification (async)
Use
createSignatureVerifier
createSignatureVerifier
jwtVerify
jwtVerify
sha256
sha256
import jwt from 'jsonwebtoken';
import { createHmac } from 'crypto';
import { createSignatureVerifier } from '@crystallize/js-api-client';
const secret = process.env.CRYSTALLIZE_SIGNATURE_SECRET!;
const verify = createSignatureVerifier({
secret,
jwtVerify: async (token, s) => jwt.verify(token, s) as any,
sha256: async (data) => createHmac('sha256', secret).update(data).digest('hex'),
});
// 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
});
import jwt from 'jsonwebtoken';
import { createHmac } from 'crypto';
import { createSignatureVerifier } from '@crystallize/js-api-client';
const secret = process.env.CRYSTALLIZE_SIGNATURE_SECRET!;
const verify = createSignatureVerifier({
secret,
jwtVerify: async (token, s) => jwt.verify(token, s) as any,
sha256: async (data) => createHmac('sha256', secret).update(data).digest('hex'),
});
// 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
});
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',
);
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 key = await files.uploadImage('/absolute/path/to/picture.jpg');
// Use `key` in subsequent PIM mutations
import { createBinaryFileManager } from '@crystallize/js-api-client';
const files = createBinaryFileManager(api);
const key = await files.uploadImage('/absolute/path/to/picture.jpg');
// Use `key` in subsequent PIM mutations
Mass Call Client
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
// import { createMassCallClient } from '@crystallize/js-api-client';
const client = createMassCallClient(api, { initialSpawn: 1 }); // api created via createClient(...)
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();
// import { createMassCallClient } from '@crystallize/js-api-client';
const client = createMassCallClient(api, { initialSpawn: 1 }); // api created via createClient(...)
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();
Full example: https://github.com/CrystallizeAPI/libraries/blob/main/components/js-api-client/src/examples/dump-tenant.ts