关于 sfcc-ocapi-scapi
面向 Salesforce B2C Commerce 开发者的技能,说明如何使用 SCAPI、Shopper APIs 和 OCAPI 构建店面及管理操作,并涵盖 SLAS 身份验证。
主要功能
- 01区分 SCAPI 与 OCAPI 的用途
- 02实施 SLAS 令牌认证
- 03查询商品目录并管理购物篮
- 04通过 OCAPI 执行服务端管理操作
- 05涵盖迁移与常见故障排查
适用场景
- 01构建无头 B2C 店面
- 02为移动应用接入商品、购物车和结账
- 03使用 OCAPI 集成订单管理
- 04将现有 OCAPI 集成迁移至 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 提供两类 API:使用 Basic Auth 或 OAuth 的旧版 Open Commerce API(OCAPI),以及使用 SLAS(Shopper Login and API Access Service)令牌的现代 Commerce API(SCAPI/Shopper API)。对于无头店面、PWA Kit 和第三方集成,推荐使用 SCAPI。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 注册了 data 资源权限,而不仅仅是 shop 资源权限 |
| 产品搜索返回 0 条结果 | 在 Business Manager → Site Preferences → Search 中检查搜索索引状态;另请确认 siteId 参数与正确的站点匹配 |
| 超出 SCAPI 速率限制(429) | 实施指数退避;考虑在服务端缓存目录数据(产品列表很少实时变化) |
相关技能
- @sfcc-cartridge-development
- @sfcc-business-manager
- @headless-commerce-architecture
- @oauth-implementation
- @shopify-storefront-api
评论
支持 Markdown