sfcc-ocapi-scapi について
Salesforce B2C Commerce の開発者向けスキルです。SCAPI、Shopper APIs、OCAPI によるストアフロントや管理操作、SLAS 認証を扱います。
主な機能
- 01SCAPI と OCAPI の用途を整理
- 02SLAS トークン認証の実装
- 03カタログ検索とバスケット管理
- 04OCAPI によるサーバー側の管理操作
- 05移行とトラブルシューティング
活用シーン
- 01ヘッドレス B2C ストアフロントの構築
- 02モバイルアプリの商品・カート・チェックアウト接続
- 03OCAPI を使った注文管理の連携
- 04OCAPI 連携から SCAPI への移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill sfcc-ocapi-scapiインストール後、Claude Code で/sfcc-ocapi-scapiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/ocapi-data-api-admin-operations-and-orde/criteria.jsontask.mdevals/scapi-headless-catalog-integration-with-/criteria.jsontask.mdevals/slas-authentication-and-session-manageme/criteria.jsontask.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 トークン管理を実装する場合
基本的な手順
-
API の全体像を把握します
API 認証 用途 SCAPI Shopper APIs SLAS ゲスト/顧客トークン ヘッドレスストアフロント、商品検索、カート、チェックアウト OCAPI Shop API Basic/OAuth 信頼できるサーバーコンテキストからのストアフロント操作 OCAPI Data API クライアント認証情報 管理操作:商品インポート、注文管理、プロモーション SCAPI Admin APIs クライアント認証情報 最新の管理操作(OCAPI Data API を段階的に置き換え) ベース URL のパターン:
https://{shortCode}.api.commercecloud.salesforce.com/ -
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(); } -
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(); } -
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" } } -
サーバーサイドの管理操作には 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 エラーを待つのではなく、有効期限が切れる前にバックグラウンドで更新します
- トークンは
httpOnlyCookie に保存します — クライアントサイドの 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