Guest Order Access with an Access Code (OTP)
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.
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.
| 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.
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.
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.
Steps 1–7 are identical; the lookup response additionally exposes eligibility so the storefront can offer cancel/return, and steps 8+ become available:
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.
The extension point is sfcc.app.order.sendOrderAccessCode. Register it in your cartridge’s
hooks.json:
…referenced from your package.json:
The hook receives the order and the generated access code, and returns a Status:
| 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"). |
| 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.
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-codeendpoint takes anemailand 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.
All endpoints live under /organizations/{organizationId}/orders/{orderNo} in the
Shopper Orders
SCAPI family and require a SLAS ShopperToken.
POST .../actions/request-access-code
| Field | Required | Description |
|---|---|---|
email | Yes | 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.
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.”
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).
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.
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):
| Field | Required | Description |
|---|---|---|
reason | No | Must match a configured cancel reason code (see oms-meta-data). Server applies the default when omitted. |
orderAccessCode | No | The code. Only needed when the current ShopperToken doesn’t already grant access to the order. |
POST .../actions/oms-return-order
| Field | Required | Description |
|---|---|---|
productItems | Yes | 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. |
| 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.
- 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-codeendpoint is always live; the flow becomes usable the moment you register thesendOrderAccessCodehook. 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-orderandoms-return-orderendpoints are OMS-backed; there is no non-OMS cancel/return path for this flow. expand=omsis required to reach OMS-resolved orders. Onrequest-access-codeandlookup, the platform only consults the OMS when the request carriesexpand=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.
- 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:
orderViewCodeon lookup,orderAccessCodeon 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
localeis available to your storefront/hook logic; the message body, subject, and language are entirely yours to build.
- Shopper Orders API:
request-access-code,lookup,oms-cancel-order,oms-return-order.