mcpField
S
sfcc-ocapi-scapi

sfcc-ocapi-scapi

github.com/finsilabs開発6080 回インストール5 日前に更新
リポジトリ

sfcc-ocapi-scapi について

Salesforce B2C Commerce の開発者向けスキルです。SCAPI、Shopper APIs、OCAPI によるストアフロントや管理操作、SLAS 認証を扱います。

主な機能

  1. 01SCAPI と OCAPI の用途を整理
  2. 02SLAS トークン認証の実装
  3. 03カタログ検索とバスケット管理
  4. 04OCAPI によるサーバー側の管理操作
  5. 05移行とトラブルシューティング

活用シーン

  1. 01ヘッドレス B2C ストアフロントの構築
  2. 02モバイルアプリの商品・カート・チェックアウト接続
  3. 03OCAPI を使った注文管理の連携
  4. 04OCAPI 連携から SCAPI への移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill sfcc-ocapi-scapi

インストール後、Claude Code で/sfcc-ocapi-scapiを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

tile.json
evals/ocapi-data-api-admin-operations-and-orde/
criteria.json
task.md
evals/scapi-headless-catalog-integration-with-/
criteria.json
task.md
evals/slas-authentication-and-session-manageme/
criteria.json
task.md

詳細

SFCC OCAPI と Shopper API

概要

Salesforce B2C Commerce には、Basic Auth または OAuth を使用する従来の Open Commerce API(OCAPI)と、SLAS(Shopper Login and API Access Service)トークンを使用する最新の Commerce API(SCAPI/Shopper API)という 2 つの API ファミリーがあります。SCAPI は、ヘッドレスストアフロント、PWA Kit、サードパーティーとの連携に推奨されるアプローチです。OCAPI は、サーバーサイドの管理操作(商品インポート、注文管理、プロモーション管理)における主要な Data API として引き続き使用されています。Composable Storefront(旧 PWA Kit)は、すべて SCAPI で構築されています。

このスキルを使用する場面

  • SFCC をコマースバックエンドとして使用するヘッドレス B2C ストアフロントを構築する場合
  • カスタム API 呼び出しを使用して Salesforce PWA Kit(Composable Storefront)を実装する場合
  • モバイルアプリを SFCC の商品カタログ、カート、チェックアウトと連携する場合
  • OCAPI Data API を使用してサーバーサイドの注文管理連携を構築する場合
  • 既存の OCAPI 連携を新しい SCAPI エンドポイントに移行する場合
  • 顧客認証フロー向けに SLAS トークン管理を実装する場合

基本的な手順

  1. API の全体像を把握します

    API認証用途
    SCAPI Shopper APIsSLAS ゲスト/顧客トークンヘッドレスストアフロント、商品検索、カート、チェックアウト
    OCAPI Shop APIBasic/OAuth信頼できるサーバーコンテキストからのストアフロント操作
    OCAPI Data APIクライアント認証情報管理操作:商品インポート、注文管理、プロモーション
    SCAPI Admin APIsクライアント認証情報最新の管理操作(OCAPI Data API を段階的に置き換え)

    ベース URL のパターン:https://{shortCode}.api.commercecloud.salesforce.com/

  2. SLAS(Shopper Login and API Access Service)で認証します

    // lib/sfcc-auth.ts
    const SLAS_BASE = `https://${process.env.SFCC_SHORT_CODE}.api.commercecloud.salesforce.com/shopper/auth/v1`;
    const ORG_ID = process.env.SFCC_ORG_ID!; // f_ecom_xxx format
    const CLIENT_ID = process.env.SFCC_SLAS_CLIENT_ID!;
    
    // Step 1: Get PKCE code challenge for guest token
    function generateCodeChallenge(): { verifier: string; challenge: string } {
      const nodeCrypto = require("crypto");
      const verifier = nodeCrypto.randomBytes(32).toString("hex");
      const hash = nodeCrypto.createHash("sha256").update(verifier).digest("base64url");
      return { verifier, challenge: hash };
    }
    
    // Get a guest access token
    export async function getGuestToken(): Promise<{ access_token: string; refresh_token: string }> {
      const { verifier, challenge } = generateCodeChallenge();
    
      // Step 1: Authorize (get auth code)
      const authorizeUrl = new URL(`${SLAS_BASE}/organizations/${ORG_ID}/oauth2/authorize`);
      authorizeUrl.searchParams.set("client_id", CLIENT_ID);
      authorizeUrl.searchParams.set("channel_id", process.env.SFCC_SITE_ID!);
      authorizeUrl.searchParams.set("redirect_uri", process.env.SFCC_SLAS_REDIRECT_URI!);
      authorizeUrl.searchParams.set("response_type", "code");
      authorizeUrl.searchParams.set("code_challenge", challenge);
      authorizeUrl.searchParams.set("hint", "guest");
    
      // SFCC returns a redirect — extract the code from the Location header
      const authResponse = await fetch(authorizeUrl.toString(), { redirect: "manual" });
      const location = authResponse.headers.get("location") ?? "";
      const code = new URL(location).searchParams.get("code") ?? "";
    
      // Step 2: Exchange code for token
      const tokenResponse = await fetch(
        `${SLAS_BASE}/organizations/${ORG_ID}/oauth2/token`,
        {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "authorization_code_pkce",
            code,
            code_verifier: verifier,
            client_id: CLIENT_ID,
            redirect_uri: process.env.SFCC_SLAS_REDIRECT_URI!,
            channel_id: process.env.SFCC_SITE_ID!,
          }),
        }
      );
    
      return tokenResponse.json();
    }
    
    // Refresh an access token
    export async function refreshToken(refreshToken: string) {
      const response = await fetch(`${SLAS_BASE}/organizations/${ORG_ID}/oauth2/token`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: refreshToken,
          client_id: CLIENT_ID,
        }),
      });
      return response.json();
    }
    
  3. Shopper Products API で商品カタログを検索します

    // lib/sfcc-products.ts
    const API_BASE = `https://${process.env.SFCC_SHORT_CODE}.api.commercecloud.salesforce.com`;
    const ORG_ID = process.env.SFCC_ORG_ID!;
    const SITE_ID = process.env.SFCC_SITE_ID!;
    
    // Search products
    export async function searchProducts(params: {
      q?: string;
      categoryId?: string;
      start?: number;
      count?: number;
      sortKey?: string;
    }, accessToken: string) {
      const url = new URL(`${API_BASE}/search/shopper-search/v1/organizations/${ORG_ID}/product-search`);
      url.searchParams.set("siteId", SITE_ID);
      if (params.q) url.searchParams.set("q", params.q);
      if (params.categoryId) url.searchParams.set("refine", `cgid=${params.categoryId}`);
      url.searchParams.set("start", (params.start ?? 0).toString());
      url.searchParams.set("count", (params.count ?? 20).toString());
      if (params.sortKey) url.searchParams.set("sort", params.sortKey);
      url.searchParams.set("expand", "images,prices,availability,variations");
    
      const response = await fetch(url.toString(), {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      return response.json();
    }
    
    // Get product by ID
    export async function getProduct(productId: string, accessToken: string) {
      const url = `${API_BASE}/product/shopper-products/v1/organizations/${ORG_ID}/products/${productId}?siteId=${SITE_ID}&expand=images,prices,availability,variations,promotions`;
      const response = await fetch(url, {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      return response.json();
    }
    
  4. Shopper Baskets API でカートとチェックアウトを管理します

    // lib/sfcc-basket.ts
    
    // Create a basket
    export async function createBasket(accessToken: string) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets?siteId=${SITE_ID}`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ customerInfo: { email: null } }),
        }
      );
      return response.json(); // { basketId: "xxx", ... }
    }
    
    // Add item to basket
    export async function addItemToBasket(
      basketId: string,
      productId: string,
      quantity: number,
      accessToken: string
    ) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets/${basketId}/items?siteId=${SITE_ID}`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify([{ productId, quantity }]),
        }
      );
      return response.json();
    }
    
    // Set shipping address
    export async function setShipmentAddress(
      basketId: string,
      shipmentId = "me",
      address: Record<string, string>,
      accessToken: string
    ) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets/${basketId}/shipments/${shipmentId}/shipping-address?siteId=${SITE_ID}`,
        {
          method: "PUT",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(address),
        }
      );
      return response.json();
    }
    
    // Submit order
    export async function submitOrder(basketId: string, accessToken: string) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-orders/v1/organizations/${ORG_ID}/orders?siteId=${SITE_ID}`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ basketId }),
        }
      );
      return response.json(); // { orderNo: "00001234", status: "created" }
    }
    
  5. サーバーサイドの管理操作には OCAPI Data API を使用します

    OCAPI Data API では、クライアント認証情報を使用する OAuth2 トークンを使用します。

    // lib/sfcc-ocapi.ts
    
    // Get admin token via client credentials
    async function getAdminToken(): Promise<string> {
      const response = await fetch("https://account.demandware.com/dwsso/oauth2/access_token", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${Buffer.from(
            `${process.env.SFCC_OCAPI_CLIENT_ID}:${process.env.SFCC_OCAPI_CLIENT_SECRET}`
          ).toString("base64")}`,
        },
        body: new URLSearchParams({ grant_type: "client_credentials" }),
      });
      const { access_token } = await response.json();
      return access_token;
    }
    
    // Get order via OCAPI Data API
    export async function getOrder(orderNo: string): Promise<Record<string, unknown>> {
      const token = await getAdminToken();
      const instanceUrl = process.env.SFCC_INSTANCE_URL!; // https://xxxx.dx.commercecloud.salesforce.com
    
      const response = await fetch(
        `${instanceUrl}/s/${process.env.SFCC_SITE_ID}/dw/data/v23_2/orders/${orderNo}`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
          },
        }
      );
      return response.json();
    }
    
    // Update order status
    export async function updateOrderStatus(orderNo: string, status: string): Promise<void> {
      const token = await getAdminToken();
      const instanceUrl = process.env.SFCC_INSTANCE_URL!;
    
      await fetch(
        `${instanceUrl}/s/${process.env.SFCC_SITE_ID}/dw/data/v23_2/orders/${orderNo}`,
        {
          method: "PATCH",
          headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ status }),
        }
      );
    }
    

例

顧客のログインとカートのマージ(SCAPI)
export async function loginCustomer(
  email: string,
  password: string,
  guestToken: string
): Promise<{ access_token: string; customer_id: string }> {
  // Step 1: Get login token via Trusted Agent SLAS flow
  const response = await fetch(
    `${SLAS_BASE}/organizations/${ORG_ID}/oauth2/login`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Authorization: `Bearer ${guestToken}`,
      },
      body: new URLSearchParams({
        type: "credentials",
        username: email,
        password,
        client_id: CLIENT_ID,
      }),
    }
  );

  const { access_token, customer_id } = await response.json();

  // Step 2: Merge guest basket into customer basket
  if (guestBasketId) {
    await fetch(
      `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets?siteId=${SITE_ID}`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${access_token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ guestBasketId }), // Triggers merge
      }
    );
  }

  return { access_token, customer_id };
}
Data API を使用した OCAPI の商品インポート
// Import/update products via OCAPI Data API
export async function upsertProduct(product: {
  id: string;
  name: string;
  longDescription?: string;
  price: number;
}) {
  const token = await getAdminToken();
  const instanceUrl = process.env.SFCC_INSTANCE_URL!;

  const ocapiProduct = {
    id: product.id,
    name: { default: product.name },
    long_description: { default: product.longDescription ?? "" },
    price: product.price,
    classification_category: { id: "electronics" },
  };

  const response = await fetch(
    `${instanceUrl}/s/-/dw/data/v23_2/products/${product.id}`,
    {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(ocapiProduct),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`OCAPI product upsert failed: ${error.message}`);
  }

  return response.json();
}

ベストプラクティス

  • 新しいヘッドレスプロジェクトでは、すべて SCAPI/Shopper API を使用します — OCAPI はメンテナンスモードです。SCAPI はレート制限がより優れており、JWT ベースの認証を使用し、Salesforce による積極的な投資が行われています
  • トークンの更新を事前に実装します — SLAS アクセストークンの有効期限は 30 分です。401 エラーを待つのではなく、有効期限が切れる前にバックグラウンドで更新します
  • トークンは httpOnly Cookie に保存します — クライアントサイドの JavaScript から SLAS トークンに直接アクセスさせないでください。サーバーサイドで Cookie を設定し、API 呼び出しにはサーバーサイドプロキシを使用します
  • Salesforce Commerce SDK(React では @salesforce/commerce-sdk-react、Node.js では commerce-sdk-isomorphic)を使用して、自動トークン管理機能を備えた型安全な API クライアントを利用します
  • OCAPI のクライアント認証情報を公開しないでください — Data API の認証情報には管理者レベルのアクセス権があります。環境変数とサーバーサイド専用のコードパスを使用します
  • select パラメーターを使用してフィールドを制限します — SFCC API のレスポンスは大きいため、?select=(id,name,price_min) を使用すると、リストエンドポイントのレスポンスサイズを大幅に削減できます
  • 注文送信にはリクエストの冪等性を実装します — SFCC のバスケットでは競合状態が発生する可能性があります。注文送信時には Idempotency-Key ヘッダーを使用します

よくある問題

問題解決方法
SLAS の認証で 400 が返されるBusiness Manager の SLAS 設定に redirect_uri が登録されていることを確認してください。URI は完全に一致している必要があります
ゲストトークンで公開済みの商品が表示されないBusiness Manager → Site Preferences → OCAPI Settings で、ゲストのクライアント ID に対する OCAPI/Shop API の権限を確認してください
バスケットの統合が失敗しても通知されないゲストのバスケットは、顧客セッションと同じ SLAS クライアント ID で作成する必要があります。異なるクライアント間でのバスケット統合はサポートされていません
有効なトークンでも OCAPI が 401 を返すOCAPI Data API では、クライアント ID に shop リソースだけでなく、data リソースの権限も登録する必要があります
商品検索で結果が 0 件になるBusiness Manager → Site Preferences → Search で検索インデックスの状態を確認してください。また、siteId パラメーターが正しいサイトと一致していることも確認してください
SCAPI のレート制限を超過した(429)指数バックオフを実装してください。カタログデータはリアルタイムで変更されることが少ないため、サーバー側でのキャッシュも検討してください

関連スキル

  • @sfcc-cartridge-development
  • @sfcc-business-manager
  • @headless-commerce-architecture
  • @oauth-implementation
  • @shopify-storefront-api

コメント