commercetools TypeScript SDK

Description

Core commercetools API and SDK patterns — TypeScript SDK setup, ClientBuilder authentication, project data model (products, customers, orders, types, channels, stores), GraphQL vs REST patterns, query predicates, optimistic concurrency, rate limits, and platform observability. Foundational layer that storefront, MC app, Connect, and integration skills all reference. Use for any commercetools project regardless of surface.

Installation

Recommended: install the full commercetools plugin. It includes this Skill, every other commercetools Skill, our pre-tuned Subagents, and the commercetools Knowledge MCP — which gives AI live access to the commercetools docs, GraphQL/OpenAPI schemas, and query validation. You only install once; every Skill on this site becomes available in every session.
Install the plugin

In any Claude Code session:

/plugin marketplace add commercetools/commercetools-ai-plugins
/plugin install commercetools@commercetools
Reload plugins

If you've updated the plugin or installed it in another window and need the current session to pick up the latest version:

/reload-plugins
Claude Desktop
Customize -> Personal plugins -> Create plugin -> Add marketplace -> Add commercetools/commercetools-ai-plugins. Then, click on the plugin and click Install.

Instructions Included

SKILL.md

commercetools TypeScript SDK

Foundational patterns for connecting to the commercetools API from TypeScript. These patterns are project-type agnostic — they apply whether you are building a Next.js storefront, a serverless function, or a CLI script.

Workflow

When this skill is invoked, always follow these steps:

  1. Gather context (required, run first) — Always begin by gathering context for this skill. This is the mandatory grounding step: it gathers the latest verified documentation as context for you (the agent). Do not skip it, and do not replace it with another tool (such as an MCP documentation-search tool) — run this command:
    node scripts/gather-context.mjs \
      --query "<extract key terms from user's question>" \
      --client-name "<current-client>" \
      --model "<current-model>" \
      --skill-name "commercetools-platform" \
      --limit 3
    
    Use its output as your primary grounding. You may additionally use other tools (such as the commercetools documentation MCP) for deeper, follow-up search.
  2. Combine with skill references — Cross-reference the analysis output with local references in ./references/ for complete context.
  3. Provide implementation guidance — Synthesize the documentation with the specific integration mode the user is targeting.

SDK Setup

See sdk-setup.md for:
  • Package installation (@commercetools/platform-sdk + @commercetools/ts-client)
  • ClientBuilder singleton with Client Credentials flow
  • Required environment variables and auth URLs by region
  • Required API client scopes

Product Search API

  • Official docs and why the legacy productProjections search is deprecated
  • Full-text + filter + sort + facets example
  • Category filter, SKU lookup, price selection, discount expansion, BOPIS channel filtering

References

product-search.md

Product Search API

Impact: HIGH — The legacy productProjections search endpoint is deprecated and lacks facets and proper variant matching. Always use the Product Search API.

Table of Contents


INCORRECT:
// Deprecated — no facets, no proper variant matching
await apiRoot.productProjections().search().get({ queryArgs: { ... } }).execute();
CORRECT:
// Product Search API — use this always
await apiRoot.products().search().post({ body: { ... } }).execute();
The Product Search API uses a POST body for the query, not URL query args.

Pattern 2: Strong Typing — Never Use any or unknown

INCORRECT: casting to any or unknown to work around missing types.
// BAD — loses all type safety
const name = (result as any).productProjection?.name?.['en-US'];
const discount = (ctPrice.discounted?.discount?.obj as any)?.name;
CORRECT: import and use the SDK types from @commercetools/platform-sdk directly.
import type {
  ProductSearchResult,
  ProductProjection,
  ProductVariant,
  Price as CtPrice,
  ProductDiscount,
  LocalizedString,
} from '@commercetools/platform-sdk';

// Result typing
const result: ProductSearchResult = body.results[0];
const projection: ProductProjection | undefined = result.productProjection;

// Expanded discount reference — obj is typed as ProductDiscount | undefined
const discountObj = ctPrice.discounted?.discount?.obj as ProductDiscount | undefined;
const discountName = getLocalizedString(discountObj?.name as LocalizedString | undefined, locale);
The @commercetools/platform-sdk exports types for every resource, reference, and expanded object in the API. Search the package exports before reaching for any.

Pattern 3: Full Example — Text + Filter + Sort + Facets

import { apiRoot } from './client';
import type { ProductSearchRequest } from '@commercetools/platform-sdk';

const searchRequest: ProductSearchRequest = {
  query: {
    and: [
      {
        fullText: {
          field: 'name',
          language: 'en-US', // Always use BCP-47
          value: 'cotton shirt',
        },
      },
      {
        filter: [
          {
            exact: {
              field: 'variants.attributes.color',
              fieldType: 'ltext',
              language: 'en-US', // Always use BCP-47
              value: 'Blue',
            },
          },
        ],
      },
    ],
  },
  sort: [
    { field: 'name', language: 'en-US', order: 'asc' },
  ],
  facets: [
    {
      distinct: {
        name: 'categories',
        field: 'categories.id',
      },
    },
    {
      distinct: {
        name: 'sizes',
        field: 'variants.attributes.size',
        fieldType: 'enum',
      },
    },
    {
      ranges: {
        name: 'price-ranges',
        field: 'variants.prices.centAmount',
        ranges: [
          { from: 0,    to: 2000 },
          { from: 2000, to: 5000 },
          { from: 5000 },
        ],
      },
    },
  ],
  markMatchingVariants: true,
  limit: 20,
  offset: 0,
};

const { body } = await apiRoot.products().search().post({ body: searchRequest }).execute();
// body.results[].productProjection — mapped by lib/mappers/product.ts
// body.facets — array of ProductSearchFacetResult - ask commercetools-developer-tips about ProductSearchFacetResult
// body.total, body.offset, body.limit — for pagination

Pattern 4: Category Filter

Filter to products in a category and all its subcategories using categoriesSubTree:
const { body } = await apiRoot.products().search().post({
  body: {
    query: {
      exact: {
        field: 'categoriesSubTree',
        value: categoryId,   // commercetools category ID
      },
    },
    productProjectionParameters: {
      priceCurrency: 'USD',
      priceCountry:  'US',
    },
    limit: 24,
    offset: 0,
  },
}).execute();
Use categoriesSubTree instead of categoriescategories matches only the exact category, not descendants.

Pattern 5: SKU Lookup

Fetch a single product by exact SKU match:

import type { ProductSearchRequest, ProductProjection } from '@commercetools/platform-sdk';

const { body } = await apiRoot.products().search().post({
  body: {
    query: {
      exact: {
        field: 'variants.sku',
        value: sku,
      },
    } as ProductSearchRequest['query'],
    productProjectionParameters: {
      priceCurrency: currency,
      priceCountry:  country,
      localeProjection: [locale],
    },
    limit: 1,
  },
}).execute();

const projection: ProductProjection | undefined = body.results[0]?.productProjection;
// projection is undefined when the SKU doesn't exist — call notFound() in the page

Pattern 6: Price Selection

Pass priceCurrency + priceCountry in productProjectionParameters. commercetools selects the matching price tier automatically — each variant arrives with .price already resolved to the correct currency/country combination.
productProjectionParameters: {
  priceCurrency: 'EUR',
  priceCountry:  'DE',
}
// variant.price is now the EUR price for Germany — no client-side filtering needed

Pattern 7: Discount Expansion

INCORRECT: not expanding discount references — the discount name is undefined.
CORRECT: expand masterVariant and variants discount refs, and use SDK types in the mapper:
// In the search call
productProjectionParameters: {
  priceCurrency: currency,
  priceCountry:  country,
  expand: [
    'masterVariant.price.discounted.discount',
    'variants[*].price.discounted.discount',
  ],
},
// In the price mapper — use ProductDiscount, not any
import type { Price as CtPrice, ProductDiscount, LocalizedString } from '@commercetools/platform-sdk';

function mapPrice(ctPrice: CtPrice): Price {
  const discountObj = ctPrice.discounted?.discount?.obj as ProductDiscount | undefined;
  return {
    value: mapMoney(ctPrice.value),
    discounted: ctPrice.discounted
      ? {
          value:        mapMoney(ctPrice.discounted.value),
          discountName: getLocalizedString(discountObj?.name as LocalizedString | undefined, locale),
        }
      : undefined,
  };
}
Without expansion, discount is just { id: '...' } — the obj field (the expanded resource) is absent.

Checklist

  • Using apiRoot.products().search().post() — never productProjections().search().get()
  • No any or unknown casts — types imported from @commercetools/platform-sdk
  • Category pages filter with categoriesSubTree, not categories
  • priceCurrency + priceCountry set in productProjectionParameters for correct price selection
  • SKU lookup uses exact: { field: 'variants.sku', value: sku }
  • markMatchingVariants: true set when variant-level filtering is active
  • Discount expansion added when rendering discount names or badges; mapper uses ProductDiscount type
sdk-setup.md

commercetools SDK Setup

Impact: CRITICAL — One ClientBuilder instance per process. Instantiating it per request causes token exhaustion and memory leaks.

Table of Contents


Pattern 1: Install Packages

npm install @commercetools/platform-sdk @commercetools/ts-client
PackagePurpose
@commercetools/platform-sdkTyped commercetools REST API client — apiRoot, request builders, SDK types
@commercetools/ts-clientToken management + HTTP middleware (ClientBuilder, withClientCredentialsFlow)

Pattern 2: SDK Client Singleton

INCORRECT: new ClientBuilder() inside a page, component, Route Handler, or lambda invocation — creates a new HTTP client and OAuth token per call.
CORRECT — one module-level singleton, imported everywhere:
// lib/ct/client.ts
import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
import { ClientBuilder } from '@commercetools/ts-client';

const projectKey = process.env.CTP_PROJECT_KEY!;
const authUrl    = process.env.CTP_AUTH_URL!;
const apiUrl     = process.env.CTP_API_URL!;

function buildClient() {
  return new ClientBuilder()
    .withProjectKey(projectKey)
    .withClientCredentialsFlow({
      host: authUrl,
      projectKey,
      credentials: {
        clientId:     process.env.CTP_CLIENT_ID!,
        clientSecret: process.env.CTP_CLIENT_SECRET!,
      },
      scopes: [process.env.CTP_SCOPES!],
    })
    .withHttpMiddleware({ host: apiUrl })
    .build();
}

export const apiRoot = createApiBuilderFromCtpClient(buildClient())
  .withProjectKey({ projectKey });

export { projectKey, apiUrl, authUrl };
withClientCredentialsFlow handles OAuth 2.0 token fetching and auto-refresh transparently — you never call the auth endpoint directly.
Every helper function imports apiRoot from this file:
import { apiRoot } from './client';

export async function getSomething(id: string) {
  const { body } = await apiRoot.things().withId({ ID: id }).get().execute();
  return body;
}

Pattern 3: Environment Variables

INCORRECT: Example Nextjs NEXT_PUBLIC_CTP_CLIENT_SECRET — exposes the secret in the browser bundle.
CORRECT — all commercetools variables are server-only (no NEXT_PUBLIC_ prefix):
# .env  (add to .gitignore — never commit)
CTP_PROJECT_KEY=your-project-key
CTP_AUTH_URL=https://auth.us-central1.gcp.commercetools.com
CTP_API_URL=https://api.us-central1.gcp.commercetools.com
CTP_CLIENT_ID=your-client-id
CTP_CLIENT_SECRET=your-client-secret
# B2C example
CTP_SCOPES=manage_order_edits:your-project-key view_sessions:your-project-key view_product_selections:your-project-key view_shipping_methods:your-project-key manage_shopping_lists:your-project-key view_discount_codes:your-project-key manage_customers:your-project-key view_types:your-project-key manage_sessions:your-project-key manage_orders:your-project-key view_standalone_prices:your-project-key view_tax_categories:your-project-key view_published_products:your-project-key view_cart_discounts:your-project-key create_anonymous_token:your-project-key view_project_settings:your-project-key view_products:your-project-key view_categories:your-project-key
Auth URL by region:
RegionAuth URL
Americas (GCP)https://auth.us-central1.gcp.commercetools.com
Europe (GCP)https://auth.europe-west1.gcp.commercetools.com
Australia (GCP)https://auth.australia-southeast1.gcp.commercetools.com
Required API client scopes (Merchant Center → Settings → Developer Settings): Use Frontend B2C template (or Frontend B2B), then make sure manage_sessions and manage_orders are included.

Checklist

  • lib/ct/client.ts exports a single apiRoot — no new ClientBuilder() anywhere else
  • .env is listed in .gitignore
  • No commercetools env vars exposed to client rendering component
  • All lib/ct/*.ts helper functions import apiRoot from ./client