When a guest shopper checks out without an account and later loses their session by closing the browser, switching devices, or clearing cookies, they can’t get back to the order they placed.
The Order Access Code flow gives that shopper a secure way back in. The shopper requests a 6-digit,
time-limited access code (OTP), the platform emails it to the address on the order, and the shopper
enters it to look up, cancel, or return that order through the
Shopper Orders API.
This guide is for merchants and Commerce App developers enabling guest order self-service (view,
cancel, and return) on B2C Commerce.
There is one thing you must build to make this flow work: the email delivery hook. The platform
generates, stores, expires, and validates the code for you. It does not send the email. Instead, it
hands the code to a hook you implement, and your hook delivers it to the email address on the order
(for example, via a transactional email provider or Marketing Cloud). If you don’t register the
hook, no code is ever delivered and the flow cannot complete.
Email is the supported channel. The flow is designed around the email address on the order. That
address is what the platform validates the request against. Other channels are out of scope today.
Note
An integrated OMS unlocks the full flow. With no OMS, the access code enables order lookup only. The
shopper can view the order, but cannot cancel or return it. Cancel and return require an integrated
Order Management System (OMS): they run through the oms-cancel-order and oms-return-order
endpoints, which are backed by OMS. To offer guests the complete self-service experience (view,
cancel, and return), you need OMS integration.
Important
What the Platform Does vs. What You Do
Concern
Owner
Generate the 6-digit code
Platform
Store the code, enforce 15-minute expiry, attempt limits, and regeneration cooldown
Platform
Validate the code on lookup / cancel / return
Platform
Match the submitted email against the order before generating a code
Platform
Restrict the flow to guest orders
Platform
Deliver the code to the shopper by email
You (the sendOrderAccessCode hook)
Compose the message body, subject, branding, localization
You
The platform is deliberately delivery-agnostic. It never sends an email itself and doesn’t ship a
default email template. Delivery is entirely your hook’s responsibility.
Capabilities With and Without OMS
The access-code mechanism is the same either way: request a code, receive it by email, submit it.
What the shopper can do with the order afterward depends on whether your orders are backed by an
integrated OMS.
Capability
Without OMS
With integrated OMS
Request an access code
Yes
Yes
Look up / view the order (lookup)
Yes
Yes
Cancel the order (oms-cancel-order)
No
Yes
Return items (oms-return-order)
No
Yes
Cancel and return are only available for orders integrated with an OMS. They run exclusively through
the oms-cancel-order and oms-return-order endpoints, which are OMS-backed. Without OMS, the
access code is a read-only key: great for letting a guest re-find and view their order, but it stops
at lookup.
Integrate an OMS to offer guests the complete self-service journey: view, cancel, and return.
Reach Orders No Longer in B2C Commerce
With an integrated OMS, the flow reaches orders that no longer exist in B2C Commerce, for example,
orders that have been purged or archived out of the B2C Commerce order store but are still held in
the OMS. When the order isn’t found in B2C Commerce, resolution falls back to the OMS, so both the
access code request (email validation) and the lookup succeed against the OMS copy. Cancel and
return are OMS-backed to begin with, so they operate on the OMS order directly.
This makes the access code a durable way for a guest to reach an old order long after B2C Commerce
would have dropped it, as long as the request carries expand=oms. Without an OMS integration, an
order missing from B2C Commerce is simply a 404.
Flow Overview
Lookup Only (No OMS)
11. Shopper enters order number + email on your "Track Order" page.22. Storefront → POST .../orders/{orderNo}/actions/request-access-code Body: { email }33. Platform verifies the email matches the order, generates a 6-digit code,4 stores it in a 15-minute window, and calls your sendOrderAccessCode hook.54. Your hook emails the code to the shopper.65. Platform returns 202 Accepted (always — see "Why the response is always 202").76. Shopper enters the code.87. Storefront → POST .../orders/{orderNo}/lookup Body: { orderViewCode: "<code>" }9 → returns the full order for the shopper to view.
Full Self-Service (with Integrated OMS)
Steps 1–7 are identical; the lookup response additionally exposes eligibility so the storefront can
offer cancel/return, and steps 8+ become available:
17. Storefront → POST .../orders/{orderNo}/lookup Body: { orderViewCode: "<code>" }2 → returns the full order; storefront shows Cancel / Return where eligible.38. Storefront → POST .../orders/{orderNo}/actions/oms-cancel-order Body: { orderAccessCode, reason }4 or POST .../orders/{orderNo}/actions/oms-return-order Body: { orderAccessCode, productItems }
The same code works for lookup and the follow-up cancel/return, as long as all calls happen within
the 15-minute window. The shopper requests the code once.
Implement the Delivery Hook
Register the Extension Point
The extension point is sfcc.app.order.sendOrderAccessCode. Register it in your cartridge’s
hooks.json:
The hook receives the order and the generated access code, and returns a Status:
1"use strict";2var Status = require("dw/system/Status");34exports.sendOrderAccessCode = function(order, accessCode){5 // order — the order the code is for (see "Hook Parameters")6 // accessCode — the freshly generated 6-digit code, for example, "847291"78 var email = order.customerInfo.email; // the address on the order910 // Compose and send by email — for example, transactional email provider or Marketing Cloud.11 MyMailer.send({12 to: email,13 subject: "Your order access code",14 body: "Your access code is " + accessCode + ". It expires in 15 minutes.",15});1617 return new Status(Status.OK);18};
Hook Parameters
Argument
Type
Description
order
Order
The full order the code was requested for. Use order.customerInfo.email for the recipient and order.orderNo in the message. All standard order fields are populated.
accessCode
String
The generated code, always 6 digits, matching ^\d{6}$ (for example, "847291").
Return Values
Your hook returns
Endpoint result
Status.OK
202 Accepted. Normal success.
(hook not registered)
202 Accepted, but no code is delivered. The flow is a dead end until you register it.
Status.ERROR
A delivery-failure error surfaces to the caller (not masked as 202).
Delivery failures are intentionally not hidden behind the enumeration-safe 202 response, so that a
genuinely broken mailer is observable to your storefront rather than failing silently.
Your hook runs only when the order/email combination is valid, so returning Status.ERROR surfaces
a non-202 error and thereby reveals that this order/email was valid, the exact enumeration signal the
always-202 response exists to hide. Never use it to signal “order/email didn’t match” (the platform
already returns a uniform 202 for that, before your hook is called). Reserve it strictly for genuine
delivery failures (for example, your mailer is down), and even then, note that the error is
observable to the caller. When in doubt, return Status.OK and handle delivery problems out of band
(logging, retries, alerting) so the 202 contract stays intact.
Deliver Through a Non-Email Channel
The hook is technically channel-agnostic. Nothing stops you from reading a phone number off the order
and sending the code by SMS. But the flow is designed for email only today, and there are two things
to be aware of before going down that path:
The platform validates the request against the order’s email, not a phone number. The
request-access-code endpoint takes an email and matches it against the address on the order.
There is no phone-number equivalent, so an SMS channel would inherit email-based authorization
while delivering to a number the platform never checked.
You own the trust implications. Because the phone number isn’t validated in the flow, sending the
code by SMS is entirely at your discretion and risk.
Treat SMS or any non-email channel as an unsupported extension for now, not part of the intended
design.
Call the Shopper Orders Endpoints
All endpoints live under /organizations/{organizationId}/orders/{orderNo} in the
Shopper Orders
SCAPI family and require a SLAS ShopperToken.
Must match the email on the order (exact, case-sensitive) for a code to be generated and sent.
Response
Meaning
202
Accepted. Always returned when the request is well-formed (see below).
400
The email field is missing or empty.
Add expand=oms to include the integrated OMS. With expand=oms on the query string, email
validation resolves the order from the integrated OMS when it isn’t found in B2C Commerce. Omit it
and the code request considers only the B2C Commerce order store.
Note
Why the response is always 202. The endpoint returns 202 whether or not the order + email
combination is valid, and whether or not a code was actually generated. This prevents an attacker
from discovering valid order/email pairs, or probing cooldown state, by watching for response
differences. Your storefront should always tell the shopper “If that order and email match, we’ve
sent you a code,” never “code sent” vs. “no such order.”
Look Up the Order
POST .../lookup
Full order access with the code uses the orderViewCode field (the same field the legacy,
long-lived OrderViewCode used; the server tells the two apart by format, so both keep working).
Without Integrated OMS
The lookup resolves the order from the B2C Commerce order store only. The shopper can view the
order, but the response does not carry the OMS-sourced eligible quantities
(quantityAvailableToCancel, quantityAvailableToReturn), and cancel/return are not available.
Orders that no longer exist in B2C Commerce return 404.
Add expand=oms to the query string. The lookup then resolves the order from the integrated
OMS, including orders no longer present in B2C Commerce, and the response carries the per-item
eligible quantities (quantityAvailableToCancel, quantityAvailableToReturn), so your storefront
can show Cancel/Return only where the action is actually possible.
Intended flow: always load the order via lookup first, then offer cancel/return based on the
eligibility in that response.
Note
Cancel an Order (Requires Integrated OMS)
POST .../actions/oms-cancel-order
Cancel and return are OMS-backed and only work when your orders are integrated with an OMS. Both
take the code in the orderAccessCode field (note: a different field name than lookup):
Items to return. quantity must not exceed quantityAvailableToReturn.
productItems[].reason
No
Must match a configured return reason code. Server applies the default when omitted.
orderAccessCode
No
The code. Only needed when the ShopperToken doesn’t already grant access.
Access Code Rules
Rule
Value
Code format
6 numeric digits (000000–999999)
Validity
15 minutes from generation
Reusable within the window
Yes, a successful lookup does not consume the code; the same code works for a following cancel/return
Failed-attempt limit
3 wrong entries → the code is invalidated immediately
Regeneration cooldown
A new code cannot be generated for 15 minutes after the last one, even if it was invalidated early by failed attempts
Single active code
Only one valid code exists per order at any time
What this means:
If the shopper enters the wrong code 3 times, tell them to request a new code, but they must wait
out the 15-minute cooldown before a new one can be issued.
A 404 is always a generic “order not found.” It never states why, so don’t parse it for a reason.
Interpret it from the sequence of calls instead:
If earlier calls with the same code returned 200 and a later one suddenly returns 404, the
shopper has lost access. The code expired mid-flow. Prompt them to request a new code.
If there was never a 200, the code is likely wrong or was invalidated early (for example, by too
many failed attempts). Have the shopper re-check the code, or request a new one.
The code is delivered in the email body as plain text, not as a link. Don’t build magic-link flows
around it. Putting the code in a URL would expose it in server, proxy, and browser logs.
Limitations
Guest orders only. The platform rejects an access code submitted for an order owned by a
registered customer. Registered shoppers use the standard login-based order access. There is no
merchant configuration to change this today.
Email is the trust boundary. The code goes to whatever email is on the order. Anyone with access
to that inbox (within 15 minutes) can access the order. This is by design and matches standard OTP
delivery.
No feature toggle to “turn on.” There is no site preference to enable. The request-access-code
endpoint is always live; the flow becomes usable the moment you register the sendOrderAccessCode
hook. Without the hook, no code is delivered.
Cancel and return require an integrated OMS. Without OMS, the access code enables order lookup
only. The oms-cancel-order and oms-return-order endpoints are OMS-backed; there is no non-OMS
cancel/return path for this flow.
expand=oms is required to reach OMS-resolved orders. On request-access-code and lookup, the
platform only consults the OMS when the request carries expand=oms. This is what lets email
validation and lookup succeed against an OMS copy, including orders no longer present in B2C
Commerce. Cancel and return are OMS-native and don’t take this parameter.
Best Practices
The hook is not optional for this flow. No hook → no email → the shopper never gets a code. This is
the single most common reason “the flow doesn’t work.”
Be careful with Status.ERROR. It breaks the uniform 202 and can leak whether an order/email was
valid, so reserve it for genuine delivery failures. See Return Values for the
full rationale.
Mind the two field names: orderViewCode on lookup, orderAccessCode on cancel/return.
Don’t message “no such order” on the request step. Always give the neutral “if it matches, we sent
a code” response to preserve enumeration protection.
Localize inside your hook. The platform renders no content. The request’s locale is available to
your storefront/hook logic; the message body, subject, and language are entirely yours to build.