---
schema_version: "1"
id: 13dd7a985eb6e1611466ed35c2c56f3fd15e6d399af134f69e8a145b9bfde66e
title: Guest Order Access with an Access Code (OTP)
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/commerce-api/guides/guest-order-access-code.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/guest-order-access-code.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 1f9541f79cccb8990201dd65386efb17e7247b3b82867fda81cea6d19f282f58
products:
  - Composable Storefront
  - Salesforce APIs
---
# 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](https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=Summary).
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.

**note:** 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.

**important:** 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.

## 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)

```
1. Shopper enters order number + email on your "Track Order" page.
2. Storefront → POST .../orders/{orderNo}/actions/request-access-code   Body: { email }
3. Platform verifies the email matches the order, generates a 6-digit code,
   stores it in a 15-minute window, and calls your sendOrderAccessCode hook.
4. Your hook emails the code to the shopper.
5. Platform returns 202 Accepted (always — see "Why the response is always 202").
6. Shopper enters the code.
7. Storefront → POST .../orders/{orderNo}/lookup            Body: { orderViewCode: "<code>" }
   → 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:

```
7. Storefront → POST .../orders/{orderNo}/lookup            Body: { orderViewCode: "<code>" }
   → returns the full order; storefront shows Cancel / Return where eligible.
8. Storefront → POST .../orders/{orderNo}/actions/oms-cancel-order   Body: { orderAccessCode, reason }
   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`:

```json
{
  "hooks": [{ "name": "sfcc.app.order.sendOrderAccessCode", "script": "./order/sendAccessCode.js" }]
}
```

…referenced from your `package.json`:

```json
{ "hooks": "./hooks.json" }
```

### Implement the Script

The hook receives the order and the generated access code, and returns a `Status`:

```js
"use strict";
var Status = require("dw/system/Status");

exports.sendOrderAccessCode = function (order, accessCode) {
  // order      — the order the code is for (see "Hook Parameters")
  // accessCode — the freshly generated 6-digit code, for example, "847291"

  var email = order.customerInfo.email; // the address on the order

  // Compose and send by email — for example, transactional email provider or Marketing Cloud.
  MyMailer.send({
    to: email,
    subject: "Your order access code",
    body: "Your access code is " + accessCode + ". It expires in 15 minutes.",
  });

  return new Status(Status.OK);
};
```

### 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](https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=Summary)
SCAPI family and require a SLAS `ShopperToken`.

### Request an Access Code

`POST .../actions/request-access-code`

```sh
curl "https://{shortCode}.api.commercecloud.salesforce.com/checkout/shopper-orders/v1/organizations/{organizationId}/orders/{orderNo}/actions/request-access-code?siteId={siteId}" \
  -X POST \
  -H "Authorization: Bearer {access_token}" \
  -H "Content-Type: application/json" \
  -d '{ "email": "shopper@example.com" }'
```

| 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.                                 |

**note:** 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."

### 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.

```sh
curl ".../orders/{orderNo}/lookup?siteId={siteId}" \
  -X POST -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" \
  -d '{ "orderViewCode": "847291" }'
```

#### With Integrated OMS (`expand=oms`)

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.

```sh
curl ".../orders/{orderNo}/lookup?siteId={siteId}&expand=oms" \
  -X POST -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" \
  -d '{ "orderViewCode": "847291" }'
```

**note:** Intended flow: always load the order via `lookup` first, then offer cancel/return based on the
eligibility in that response.

### 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):

```sh
curl ".../orders/{orderNo}/actions/oms-cancel-order?siteId={siteId}" \
  -X POST -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" \
  -d '{ "reason": "Changed mind", "orderAccessCode": "847291" }'
```

| 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.           |

### Return Items (Requires Integrated OMS)

`POST .../actions/oms-return-order`

```sh
curl ".../orders/{orderNo}/actions/oms-return-order?siteId={siteId}" \
  -X POST -H "Authorization: Bearer {access_token}" -H "Content-Type: application/json" \
  -d '{
        "productItems": [ { "itemId": "10uVF0000003W0BYAU", "quantity": 1.0, "reason": "Defect" } ],
        "orderAccessCode": "847291"
      }'
```

| 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.          |

## 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](#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.

## See Also

- [Shopper Orders API](https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-orders?meta=Summary): `request-access-code`,
  `lookup`, `oms-cancel-order`, `oms-return-order`.
- [Connect a Third-Party OMS](/docs/commerce/b2c-commerce/guide/connect-third-party-oms.md#guest-order-access-codes-otp)
