OCC is the REST API of SAP Commerce Cloud, served under /occ/v2. This skill covers the frontend
side:
- calling OCC correctly from Angular, React / Next.js or Vue / Nuxt;
- where tokens and cart ids live;
- the SAP-specific behaviour that breaks ordinary REST assumptions.
Backend work (Java controllers, WsDTOs, ImpEx, extensions) belongs to a backend skill. Customizing Spartacus / Composable Storefront belongs to SAP's own Spartacus skill.
It targets SAP Commerce Cloud 2211-jdk21, the September 2025 framework update and later. JDK17
releases can no longer be built. Their OAuth differs, see references/auth.md → Old patterns.
The facts in these references were checked against SAP Help, the official OCC OpenAPI spec,
Spartacus' source and a running 2211-jdk21 instance. Use them directly instead of researching them
again. Spend research time on what they don't cover, and on this project's own backend: its
/occ/v2/api-docs and its configuration.
1. Orient before writing code
- Find the site context. Call
GET {host}/occ/v2/basesites?fields=FULL. It needs no token and no site segment. Note the base site uid,defaultLanguage, the store currencies,requiresAuthentication(typical for B2B) andurlPatterns. - Get this backend's API contract. Call
GET {host}/occ/v2/api-docs(OpenAPI 3, unauthenticated) and generate types from it, for example withnpx openapi-typescript <spec-url> -o src/occ/occ.d.ts. Projects add fields and endpoints, so this spec is the contract, not your memory of OCC. Don't invent fields. If the spec is unreachable, ask for it, or call the endpoint once withfields=FULLand read what comes back. - Read the project. Look at the framework and its version, rendering mode (SSR, SSG or SPA), HTTP client, state management, i18n and routing. Build on what is there, and don't add libraries the project doesn't use.
- Read the quirks for the area. Before you touch an area (auth, cart, search, CMS), read its
section in
references/sap-quirks.md. Most OCC bugs come from behaviour that looks like plain REST but isn't. - Check for a legacy backend. If the token endpoint accepts
grant_type=password, the backend is an older release. See Old patterns inreferences/auth.md.
2. Default architecture
| Concern | Default | Why |
|---|---|---|
| Where OCC is called | From the framework's server, as a BFF: Next.js route handlers and server actions, Nuxt server routes, the Angular SSR server | Client secrets and tokens stay out of the browser, server-to-server calls need no CORS, and token refresh happens in one place |
| Tokens | httpOnly, Secure, SameSite=Lax cookies set by the BFF | Page scripts can't read them, and the browser sends them without app code |
| Login | Authorization code + PKCE with a confidential client in the BFF. Use a custom login page (loginPageUri) if the storefront shares the authorization server's domain; otherwise use SAP's hosted login page |
2211-jdk21 has no password grant. SAP only allows a custom login page on the same domain or a subdomain. A confidential client's session slides with each refresh, while a public client's ends after the first refresh token's lifetime (one hour by default) |
| Public reads (catalog, content without a user) | Cacheable. The cache key includes site, lang and curr, plus user group where prices or CMS differ per group |
These are the expensive calls, and they rarely change per user |
| User data (cart, account, B2B prices, CMS requested with a user token) | Never in a shared cache | A cache whose key doesn't include the user (page cache, CDN, ISR, 'use cache') serves one customer's data to the next visitor, and any cache makes carts stale |
| SSR request state | Per request: site, lang, curr, token and cart id | One server instance serves many users at once |
| SPA without a server | Public client + PKCE, tokens in memory. The session ends on reload or after about an hour | See references/auth.md. When login matters, add a small BFF |
Read the framework recipe before writing framework code: references/react-nextjs.md,
references/angular.md or references/vue-nuxt.md.
3. The OCC client
Write one small client module, or extend the project's. It must:
- build URLs as
{host}/occ/v2/{baseSiteId}/{path}; - always send
langandcurr, using the isocodes from/languagesand/currencies; without them OCC falls back to the base store defaults, not the user's choice; - send
fieldsfor the use case: small for lists, larger for detail pages. Request only names that exist in this backend's spec. One unknown name fails the whole call with 400ConversionError, so a field you add for one backend can break another. The CMS endpoints (cms/pages,cms/components) accept only the levelsBASIC,DEFAULTandFULL. A list of names fails there even when every field exists; - send
Accept: application/json. OCC negotiates JSON or XML fromAccept, so don't rely on the default; - send the body in the format the endpoint expects. OCC mixes JSON, form-urlencoded and query parameters; see the endpoint tables in the references;
- turn both error formats into one error type. OCC returns
{"errors":[{"type","reason","subject","subjectType","message"}]}and OAuth returns{"error","error_description"}. Most OCC errors are HTTP 400, so branch ontypeandreason, not only on status; - refresh an expired access token once per session at a time, then retry. Access tokens live
about 5 minutes and refresh tokens rotate, so two concurrent refreshes spend the same refresh token
and log the user out (
references/auth.md); - run cart changes one at a time per cart. Concurrent writes to one cart fail, hang or duplicate
lines. A write that returned an error can still have changed the cart, so never re-send it
blindly: re-read the cart, then decide (
references/cart-checkout.md§8); - give server-side OCC calls a timeout (for example
signal: AbortSignal.timeout(8000)). Render the parts that aren't essential without OCC data when a call fails or times out, for example a header without the cart count. A slow or failing OCC must not take down every page that has a mini cart; - use the framework's own HTTP primitive: Angular
HttpClientwith interceptors, Nuxt$fetch, Next.jsfetch.
// Server-side core, framework-neutral. Adapt the fetch call to the framework recipe.
export class OccError extends Error {
constructor(readonly status: number, readonly type?: string,
readonly reason?: string, readonly subject?: string) {
super(`OCC ${status} ${type ?? ''} ${reason ?? ''}`.trim());
}
}
export type OccContext = { site: string; lang: string; curr: string; token?: string };
export async function occ<T>(ctx: OccContext, path: string, init: RequestInit = {}): Promise<T> {
const url = new URL(`${process.env.OCC_HOST}/occ/v2/${ctx.site}/${path}`); // path may carry ?fields=
url.searchParams.set('lang', ctx.lang);
url.searchParams.set('curr', ctx.curr);
const res = await fetch(url, {
...init,
headers: {
Accept: 'application/json',
...(ctx.token ? { Authorization: `Bearer ${ctx.token}` } : {}),
...init.headers,
},
});
const text = await res.text(); // some OCC calls answer 200/201 with an empty body
const body = text ? JSON.parse(text) : undefined;
if (res.ok) return body as T;
const e = body?.errors?.[0];
throw new OccError(res.status, e?.type ?? body?.error, e?.reason ?? body?.error_description, e?.subject);
}
4. Task map
| Task | Read |
|---|---|
| Login, logout, tokens, sessions, OAuth clients, custom login page | references/auth.md |
| Cart, add to cart, cart merge, checkout, guest checkout, place order | references/cart-checkout.md |
| Product pages, variants, prices, stock, images, search, facets, category pages, SEO | references/catalog-search.md |
| CMS pages and components, navigation, banners, SmartEdit, Storyblok or Contentful next to OCC | references/cms.md |
| Registration, profile, addresses, order history, passwords, consents | references/account.md |
B2B: orgUsers, account payment, cost centers, replenishment |
references/b2b.md |
| Something fails: CORS, 401, 400, missing images, empty results | references/troubleshooting.md |
| SAP behaviour that surprises frontend developers, and little-known SAP features | references/sap-quirks.md |
| Framework specifics | references/react-nextjs.md, references/angular.md, references/vue-nuxt.md |
5. Hard rules
Each rule prevents a bug that shows up in real OCC storefronts.
- Anonymous carts are addressed by
guid, customer carts bycode. Store the anonymous guid in an httpOnly cookie. Anyone who has it can read and change that cart, so keep it out of URLs and logs. - User ids in paths are
anonymousorcurrent. Usecurrentfor the signed-in customer, not their email: an id that isn't the token's own user needs customer-emulation rights, and without them the call returns 403. - HTTP 200 from add-to-cart does not mean the product was added. Read
statusCodeandquantityAddedfrom theCartModification: a low stock level can add fewer items, or none. - Show
formattedValuefor prices. It carries the store's currency and locale rules. Usevalueonly for calculations and analytics. - Media URLs are relative (
/medias/...?context=...). Prefix them with the media host and keep thecontextquery, which identifies the file. - Facet and breadcrumb links come from the response:
values[].query.query.valueandbreadcrumbs[].removeQuery.query.value. Don't assemble the colon query grammar yourself, except the initial category query:relevance:allCategories:{code}. Sort codes also come from the response (sorts[]), because they differ per site. currentPagestarts at 0.- An unknown product is HTTP 400
UnknownIdentifierError, not 404. Map it to your 404 page, or search engines index an error page. - Placing an order deletes the cart. Clear the stored cart id, create the next cart only when something is added, and block a second submit while the first is in flight.
- Guest checkout starts with the email. Until
POST …/guestusersets it, delivery modes, delivery mode and vouchers answer 401 on an anonymous cart. - Exchange each authorization code exactly once. A second exchange revokes the tokens from the first, so the customer is logged out right after signing in. A refresh, likewise, revokes the previous access token: retry in-flight calls once with the new one.
- A guest cart (email already set) can't be merged at login. See
references/cart-checkout.md. - Nothing secret in browser-visible config. That means
NEXT_PUBLIC_*,VITE_*, NuxtruntimeConfig.publicand Angularenvironment.ts, all of which ship to the browser. A client secret there is public. - Card data never passes through your code to OCC. Use the payment provider's SDK or hosted
fields and the endpoints of the project's payment extension. Ask which provider the project uses.
PSP plugins often place the order through their own endpoint, so read the plugin's OCC API first
(
references/cart-checkout.md§9). - HTML from CMS and product descriptions comes from editors. Sanitize it before rendering. Its links follow the backend's URL scheme, not your routes, so map them.
6. When the backend has to change
Frontend teams often can't change the backend. When a fix needs backend configuration, hand the backend team a ready snippet from the references, and say what it's for:
- CORS for OCC and the authorization server;
- OAuth clients (confidential for a BFF, public + PKCE for a browser app), including
loginPageUriand its allowed hosts; - token lifetimes;
- custom domains on CCv2, so the storefront and the API share a site;
- endpoints that are disabled by default.
Don't ask them to loosen security: no * origin with credentials, and no ROLE_TRUSTED_CLIENT on a
storefront client.
7. Verify
- Smoke-test the OCC calls you rely on with curl against the target backend. The commands are in
references/troubleshooting.md. - Unit-test the error mapping and the cart-id handling with recorded OCC responses, using the project's own test runner.
- If the project has end-to-end tests (Playwright, Cypress), cover this path: add to cart as a guest → log in (merge) → check out → order confirmation.