---
schema_version: "1"
id: 63f14ea73f18ca67a2f522e7eafdd417409917a2a7ea1dd4a7a65ad24b3080cb
title: Implementation Considerations
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/b2c-commerce/guides/ucp/ucp-implementation-considerations.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-implementation-considerations.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 22f7436e45a646dfc58b5fe71b8622608ee05e04d10d357d48bea5442632c827
products:
  - Composable Storefront
  - Salesforce APIs
---
# Implementation Considerations

Use this topic for UCP extensibility architecture and custom hook implementations. If you're still deciding **whether** you need custom work, start with the [Technical Audit](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-technical-audit.md).

- [UCP Extensibility Architecture](#ucp-extensibility-architecture) — when hooks fire
- [Extension Hooks Reference](#extension-hooks-reference) — hook catalog
- [UCP Checkout Session Validation](#ucp-checkout-session-validation) — flashes, status, response shaping
- [Payment Configuration](#payment-configuration) — Salesforce Payments vs third-party providers
- [Adyen Webhook Filtering for Shared Merchant Accounts](#adyen-webhook-filtering-for-shared-merchant-accounts) — Salesforce Payments (Adyen) only
- [Order Post-Processing Events](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-order-post-processing-events.md) — how OMS updates become signed UCP snapshots

## UCP Extensibility Architecture

UCP follows the standard [SCAPI extensibility with Hooks](https://developer.salesforce.com/docs/commerce/commerce-api/guide/extensibility_via_hooks.html) pattern, including [hook script execution](https://developer.salesforce.com/docs/commerce/commerce-api/guide/extensibility_via_hooks.html#hook-scripts).

**Core concept:** A UCP Checkout Session is a headless "view" on a standard Commerce Cloud [Basket](https://salesforcecommercecloud.github.io/b2c-dev-doc/docs/upcoming/scriptapi/html/index.html?target=class_dw_order_Basket.html). Standard Basket and Order hooks (`calculate`, `calculateTax`, `calculateShipping`, `validateBasket`, `createOrderNo`) run automatically. UCP isn't a second checkout to build—it's an API in front of the basket you already have. Because it's headless, storefront UI/controllers are bypassed, and **hooks registered on other SCAPI resources (Shopper Baskets, Shopper Orders, and similar) don't run**. Remap that logic onto the UCP checkout-session hooks below. The [Technical Audit](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-technical-audit.md) has the inventory checklist.

**note:** **Enable Salesforce Commerce API Hook Execution**

This Business Manager feature toggle is **required** for end-to-end UCP checkout. Checkout-session operations depend on default hook execution (`calculate`, `calculateTax`, `calculateShipping`, `validateBasket`, `createOrderNo`, and related payment hooks). If the toggle is off, those hooks don't run and a shopper can't complete a checkout session—custom third-party payment hooks don't run either. Enable it before you test or go live.

### Extensibility Models

These diagrams show how hooks are invoked during each Checkout Session operation. Each diagram shows the sequence of platform operations and hook invocation points:

#### How to Read the Diagrams

- **Boxes with labels:** Platform service operations or hook execution points
- **Flow direction:** Operations proceed left to right
- **Hook names:** Extension points where your custom code can execute
- **Multiple hooks:** May execute in sequence during a single operation

#### POST /checkout-sessions - Create Checkout Session

Opens a new UCP checkout session, creates the underlying basket.

![POST checkout-sessions extensibility model](https://developer.salesforce.com/media/b2c-commerce/ucp-create-checkout-session.png)

#### PUT /checkout-sessions/{id} - Update Checkout Session

Updates/replaces products, buyer information, fulfillment, etc. on the underlying basket.

![PUT checkout-sessions extensibility model](https://developer.salesforce.com/media/b2c-commerce/ucp-update-checkout-session.png)

#### GET /checkout-sessions/{id} - Get Checkout Session

Returns the current basket state as a checkout session.

![GET checkout-sessions extensibility model](https://developer.salesforce.com/media/b2c-commerce/get-checkout-sessions.png)

#### POST /checkout-sessions/{id}/cancel - Cancel Checkout Session

Cancel the checkout session, delete the basket (terminal).

![POST cancel checkout-sessions extensibility model](https://developer.salesforce.com/media/b2c-commerce/cancel-checkout-sessions.png)

#### POST /checkout-sessions/{id}/complete - Complete Checkout Session

Updates the billing address on the basket, creates the order, takes payment, and facilitates order placement. This operation is only available once the checkout session has the `ready_for_complete` status.

Complete uses **more than one** database transaction. Read [Transaction Management](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-transaction-management.md) before you put callouts or `Transaction.wrap()` in complete-flow hooks.

##### Complete Transaction 1

![POST complete checkout-sessions t1 extensibility model](https://developer.salesforce.com/media/b2c-commerce/ucp-complete-transaction1.png)

##### Complete Transaction 2 - SFP (Default)

![POST complete checkout-sessions sfp t2 extensibility model](https://developer.salesforce.com/media/b2c-commerce/ucp-complete-sfp-transaction2.png)

##### Complete Transaction 2 - Third-Party

![POST complete checkout-sessions 3pp t2 extensibility model](https://developer.salesforce.com/media/b2c-commerce/ucp-complete-3pp-transaction2.png)

#### SFP Adyen Notification — Async Place Order

Salesforce Payments places the order on the gateway webhook, not in `/complete`. This diagram is the Adyen `AUTHORISATION` step. Stripe Salesforce Payments is also webhook-placed (`payment_intent.succeeded`). See [If You Use Salesforce Payments](#if-you-use-salesforce-payments-default).

![Adyen notification async flow](https://developer.salesforce.com/media/b2c-commerce/adyen-notification-flow.png)

## Extension Hooks Reference

This is the hook catalog for UCP. Every Checkout Session hook returns a `dw.system.Status`.

**Transactional** is whether the hook runs inside a platform-managed database transaction:

- **Yes** — persist Script API changes (basket or order). Returning `ERROR` rolls back **that** transaction.
- **No** — the hook isn't in a transaction. Shape the response or payload only. Persisting Script API objects can cause an `ORMTransactionException`.

Complete uses more than one transaction, so **Yes** doesn't mean the whole `/complete` request rolls back. Which transaction a complete-flow hook runs in—and what an `ERROR` actually undoes—is in [Transaction Management](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-transaction-management.md).

- [Standard Basket and Order Hooks](#standard-basket-and-order-hooks)
- [Lifecycle Hooks: Create, Get, and Update](#lifecycle-hooks-create-get-and-update)
- [Cancel Hooks](#cancel-hooks)
- [Complete Hooks](#complete-hooks)
- [Payment Hooks](#payment-hooks)
- [Business Profile and Order Webhooks](#business-profile-and-order-webhooks)

For the UCP Checkout schema structure, see the [Checkout reference](https://ucp.dev/2026-04-08/specification/reference/#checkout). B2C Commerce 26.9 also serves protocol version `2026-01-23`; see [Protocol Versions](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-configuration.md#protocol-versions).

### Standard Basket and Order Hooks

These run automatically on checkout-session mutations. They are not UCP-specific extension points.

| Hook                                  | Arguments                                                                                                                   | Transactional | Typical Use                                      |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------ |
| `dw.order.calculate`                  | [Script Basket](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-script-api/dw.order.Basket.html) | Yes           | Pricing and basket totals                        |
| `dw.order.calculateTax`               | [Script Basket](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-script-api/dw.order.Basket.html) | Yes           | Tax calculation                                  |
| `dw.order.calculateShipping`          | [Script Basket](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-script-api/dw.order.Basket.html) | Yes           | Shipping calculation                             |
| `dw.ocapi.shop.basket.validateBasket` | SCAPI Basket, `onComplete`: Boolean (`true` if called on `/complete`)                                                       | Yes           | Add or remove flashes that drive checkout status |
| `dw.order.createOrderNo`              | —                                                                                                                           | Yes           | Custom order number; runs during complete        |

### Lifecycle Hooks: Create, Get, and Update

Extension point prefix: `sfcc.ucp.shopperCheckouts.v1.checkoutSessions`

| Operation | Hook                           | Transactional | Typical Use                                               |
| --------- | ------------------------------ | ------------- | --------------------------------------------------------- |
| Create    | `beforePOST(basket, request)`  | Yes           | Observe or adjust the basket before it is fully populated |
| Create    | `afterPOST(basket)`            | Yes           | Adjust the basket; the platform runs calculation here     |
| Create    | `modifyPOSTResponse(response)` | No            | Shape the response document only                          |
| Get       | `modifyGETResponse(response)`  | No            | Shape the response document only                          |
| Update    | `beforePUT(basket, request)`   | Yes           | Observe or adjust the basket before the replace           |
| Update    | `afterPUT(basket)`             | Yes           | Adjust the basket; the platform runs calculation here     |
| Update    | `modifyPUTResponse(response)`  | No            | Shape the response document only                          |

`basket` is a [Script Basket](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-script-api/dw.order.Basket.html). `request` and `response` are the UCP Checkout document.

### Cancel Hooks

Extension point prefix: `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.cancel`

| Hook                           | Transactional | Typical Use                                             |
| ------------------------------ | ------------- | ------------------------------------------------------- |
| `afterPOST(checkoutSessionId)` | Yes           | React to the cancellation; the session is being deleted |
| `modifyPOSTResponse(response)` | No            | Shape the response document only                        |

### Complete Hooks

Extension point prefix: `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.complete`

| Hook                           | Transactional | Typical Use                                                                                         |
| ------------------------------ | ------------- | --------------------------------------------------------------------------------------------------- |
| `beforePOST(basket, request)`  | Yes           | Final validation or basket adjustment before order creation                                         |
| `afterPOST(order)`             | Yes           | Post-placement work on the Order (for example DIY confirmation email). Doesn't roll back the order. |
| `modifyPOSTResponse(response)` | No            | Shape the response document only                                                                    |

`complete.afterPOST` receives a [Script Order](https://developer.salesforce.com/docs/commerce/b2c-commerce/references/b2c-script-api/dw.order.Order.html). See [Post-Placement Actions](#post-placement-actions).

### Payment Hooks

Extension point prefix: `sfcc.app.ucp.payment`

`beforeOrderCreate` runs on every complete, including Salesforce Payments. `buildPaymentInstrumentRequest` and `authorize` apply to the third-party payment provider path. See [Third-Party Payment Providers](#third-party-payment-providers). Salesforce Payments–only hooks are under [If You Use Salesforce Payments](#if-you-use-salesforce-payments-default).

| Hook                                                                   | Transactional | Typical Use                                                                                   |
| ---------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------- |
| `beforeOrderCreate(basket)`                                            | Yes           | Last chance to adjust the basket after payment instruments are built and before `createOrder` |
| `buildPaymentInstrumentRequest(basket, selectedInstrument, requestWO)` | Yes           | Translate the selected instrument into a payment instrument request (DIY)                     |
| `authorize(order, paymentInstrument)`                                  | Yes           | Authorize payment with a third-party payment service provider (DIY)                           |

### Business Profile and Order Webhooks

| Hook                                                                              | Transactional | Typical Use                                                                             |
| --------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
| `sfcc.ucp.shopperBusinessProfiles.v1.businessProfile.modifyGETResponse(response)` | No            | Replace `payment_handlers` on the well-known profile (DIY). Don't change `ucp.version`. |
| `sfcc.ucp.orderWebhook.v1.modifyEventPayload(payload)`                            | No            | Mutate the signed lifecycle payload immediately before delivery to the platform         |

If `modifyEventPayload` fails or returns a non-OK status, the webhook is retried. See [Order Post-Processing Events](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-order-post-processing-events.md) for how post-processing events become those signed snapshots.

**note:** **Considerations for Business Profile `modifyGETResponse` Hook**

The Business Profile `modifyGETResponse` hook isn't guaranteed to run on every request. B2C Commerce caches the fully resolved, post-hook Business Profile by site and UCP version, and serves that cached copy until the TTL expires. The hook runs once when the cache entry is built, not once per incoming request.

Write `modifyGETResponse` as a pure function. Don't depend on per-request data such as headers or the calling agent. If the hook fails or returns a non-OK status, the result isn't cached, and a later request rebuilds the Business Profile and runs the hook again.

The default TTL for Business Profile cache entries is 10 minutes. This value is only modifiable by Salesforce Support.

## UCP Checkout Session Validation

By default, Checkout Session APIs perform basic validations for:

- Buyer email
- Line items
- Shipping address
- Shipping method

On checkout session complete, these additional validations run:

- Billing address
- Payment method

### Adding and Removing Validation Errors

Add, edit, or remove validation errors in `dw.ocapi.shop.basket.validateBasket`.

**note:** Use [UCP standard error codes](https://developers.google.com/merchant/ucp/guides/checkout/errorcode#error_code_reference) for the `type` field so error handling stays consistent across platforms.

```javascript
exports.validateBasket = function (basketResponse, duringSubmit) {
    // Add a validation error
    basketResponse.addFlash({
        type: "eligibility_invalid",
        message: Resource.msg('error.age.verification', 'checkout', null),
        path: "$"
    });

    // Remove a validation error
    var toRemove = [];
    for each (f in basketResponse.flashes) {
        if (f.type === "InvalidBillingAddress") {
            toRemove.push(f);
        }
    }
    for (var i = 0; i < toRemove.length; i++) {
        basketResponse.removeFlash(toRemove[i]);
    }
}
```

### Flash to Message Mapping

Salesforce automatically maps Flashes to UCP Messages:

| Flash Property  | UCP Message Property |
| --------------- | -------------------- |
| `flash.type`    | `message.code`       |
| `flash.path`    | `message.path`       |
| `flash.message` | `message.content`    |

These properties are always set automatically:

- `message.type` is always `ERROR`
- `message.severity` is always `RECOVERABLE`
- `message.contentType` is always `PLAIN`

### Checkout Status Derivation

If a Basket has one or more Flashes, `Checkout.status` is `INCOMPLETE`. Otherwise, the status is `READY_FOR_COMPLETE`.

To override the standard behavior of mapping Basket and Flashes to Checkout, use any of these hooks:

- `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.modifyPOSTResponse`
- `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.modifyGETResponse`
- `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.modifyPUTResponse`
- `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.cancel.modifyPOSTResponse`
- `sfcc.ucp.shopperCheckouts.v1.checkoutSessions.complete.modifyPOSTResponse`

### Example: Custom Address Verification

Add custom address verification by using a third-party service (Loqate) in `dw.ocapi.shop.basket.validateBasket`.

```javascript
// Hook: dw.ocapi.shop.basket.validateBasket
exports.validateBasket = function (basketResponse, duringSubmit) {
  // Validate shipping address with Loqate
  var address = basketResponse.shipments[0].shippingAddress;
  if (address) {
    var addressForm = {
      Address1: address.address1,
      Address2: address.address2,
      Country: address.countryCode,
      State: address.stateCode,
      City: address.city,
      PostalCode: address.postalCode,
    };
    var response = loqate.validateLoqateAddress(addressForm);
    if (response && response.response) {
      basketResponse.addFlash({
        type: "address_undeliverable",
        message: "Item(s) in your cart can't be shipped to the selected address",
        path: "$.shipments[0].shippingAddress",
      });
    }
  }
};
```

### Filtering Store Pickup Fulfillment Methods

Use the `modifyResponse` hooks to filter out store pickup fulfillment methods when the platform doesn't support store pickup.

**Example: Don't show store pickup fulfillment methods**

```javascript
// sfcc.ucp.shopperCheckouts.v1.checkoutSessions.modifyPOSTResponse
exports.modifyPOSTResponse = function( checkoutSession )
{
    var pickupMethodIds = {};
    for each( m in ShippingMgr.getAllShippingMethods() ) {
        if ( m.custom.ucpStorePickupEnabled === true )
        {
            pickupMethodIds[m.ID] = true;
        }
    }

    for each( method in checkoutSession.fulfillment.methods ) {
        for each( group in method.groups ) {
            var filtered = [];
            for each( option in group.options ) {
                if ( !pickupMethodIds[option.id] )
                {
                    filtered.push( option );
                }
            }
            group.options = filtered;
        }
    }
}
```

## Payment Configuration

A UCP checkout is paid with a single payment instrument. How you process that payment depends on your provider, and the two paths require different work:

- **If you use Salesforce Payments (the default),** the native SFP implementation handles payment for you. See [If You Use Salesforce Payments](#if-you-use-salesforce-payments-default).
- **If you use a third-party provider (DIY),** you register a small set of hooks that advertise your payment handlers and authorize payment with your own payment service provider (PSP). See [Third-Party Payment Providers](#third-party-payment-providers).

The **Payment Provider** setting selects which path applies. See [Payment Provider Setup](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-configuration.md#payment-provider-setup).

### If You Use Salesforce Payments (Default)

The platform assembles the Google Pay / Salesforce Payments handler from Business Manager and authorizes payment for you. You don't register the DIY payment hooks. See [Payment Provider Setup](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-configuration.md#payment-provider-setup).

Authorization starts in Transaction 2, but **placement is asynchronous** for both Salesforce Payments gateways: the order stays in `CREATED` until the webhook arrives. Don't treat a successful `/complete` response as a placed order.

- **Adyen via Salesforce Payments:** placement finishes when Adyen delivers its `AUTHORISATION` webhook.
- **Stripe via Salesforce Payments:** the PaymentIntent is confirmed during Transaction 2, but placement finishes when Stripe delivers `payment_intent.succeeded`.

Optional hooks on this path:

- `dw.extensions.payments.adyenNotification` — Adyen Salesforce Payments only; runs when the Adyen notification is processed.
- `dw.extensions.payments.stripePaymentEvent` — Stripe Salesforce Payments only; runs when the Stripe payment event is processed.
- `dw.extensions.payments.sendOrderConfirmationEmail` — Salesforce Payments (Adyen or Stripe); runs after the webhook places the order.

These hooks don't fire for third-party (DIY) payment.

**note:** **Sharing an Adyen merchant account**

If you use the same Adyen merchant account for both your storefront's own third-party Adyen integration and Salesforce Payments, you must filter webhook notifications so your storefront integration ignores UCP/SFP orders it didn't create. See [Adyen Webhook Filtering for Shared Merchant Accounts](#adyen-webhook-filtering-for-shared-merchant-accounts).

### Adyen Webhook Filtering for Shared Merchant Accounts

**Why:** The Adyen merchant account would have a webhook for both the third-party and SFP configurations. Adyen delivers payment notifications to all webhook endpoints on that account. The Salesforce Payments handler already ignores non-SFP orders; your storefront Adyen integration might not, and can try to process UCP/SFP notifications it didn't create.

**note:** The code snippets here are for a third-party integration using the Adyen-managed [adyen-salesforce-commerce-cloud cartridge](https://github.com/Adyen/adyen-salesforce-commerce-cloud). Your own implementation may vary, but the same principles apply.

#### Required Action

##### 1. Tag Your Third-Party Payments with Adyen Metadata

Where you build the Adyen payment request, add a metadata field. Adyen echoes this field back in webhook notifications.

**File location:** `cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenHelper.js` (example path for Adyen cartridge)

```javascript
createAdyenRequestObject(orderNo, orderToken, paymentInstrument, customerEmail) {
    // ...
    stateData.applicationInfo = adyenHelperObj.getApplicationInfo();
    stateData.additionalData = {};

    // Tag payments created by this cartridge so webhook notifications
    // can be identified. Adyen echoes this metadata back in webhook
    // additionalData as "metadata.<key>".
    stateData.metadata = {
        paymentSource: 'adyen-sfcc-cartridge'
    };

    return stateData;
}
```

##### 2. Filter on That Metadata Field in Your Webhook Handler

Where you receive webhook events in your third-party flow, check for the metadata tag before taking any action. If absent, return `[accepted]` without doing any processing.

```javascript
function notify(req, res, next) {
  // ...
  var source = req.form["additionalData.metadata.paymentSource"];
  if (source !== "adyen-sfcc-cartridge") {
    // This payment wasn't created from this cartridge
    // Return [accepted] and do no processing
    res.render("/notify");
    return next();
  }

  // This payment had our metadata => was created by this cartridge
  // Continue with typical webhook processing flow
  Transaction.begin();
  // ...
}
```

Using a metadata field as shown here is the simplest path forward, but alternative checks exist to determine if an order was initiated from third-party or SFP, such as checking the payment method of an order. Confirm this for your third-party implementation.

#### Deployment

1. Make the code changes in your local development environment
2. Test webhook filtering with both SFP and third-party payment scenarios
3. Upload the modified cartridge to your Commerce Cloud instance via Business Manager or command-line tools
4. Add the cartridge to your site's cartridge path if not already present
5. Verify webhook handling in both Staging and Production environments

### Third-Party Payment Providers

Register the hooks below to advertise payment handlers and authorize with your own PSP. Set **Payment Provider** to **Third Party Provider (DIY)** so the platform doesn't assemble the Salesforce Payments handler. See [Payment Provider Setup](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-configuration.md#payment-provider-setup).

#### Third-Party Payment Hooks

Register these three hooks. Signatures are in [Extension Hooks Reference](#extension-hooks-reference).

- `sfcc.ucp.shopperBusinessProfiles.v1.businessProfile.modifyGETResponse` — advertise payment handlers on the well-known profile
- `sfcc.app.ucp.payment.buildPaymentInstrumentRequest` — translate the selected instrument (including its `handler_id`) into a payment instrument request
- `sfcc.app.ucp.payment.authorize` — authorize with your PSP. Fires only for a third-party instrument; Salesforce Payments bypasses this hook.

#### Advertising Payment Handlers

Register `sfcc.ucp.shopperBusinessProfiles.v1.businessProfile.modifyGETResponse` to replace the payment handlers on the well-known profile. See [Configuration and Discovery](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-configuration.md#business-profile). The hook argument exposes `ucp` and `signingKeys`. Assign handlers on `ucp.paymentHandlers`. Don't change `ucp.version` or `signingKeys`.

Copy the handler **namespace** (the map key, for example `com.google.pay`) and the handler **`id`** from the payment handler's author — usually the PSP or the platform. That `id` is what comes back on `/complete` as `handler_id`. The rest of the handler object (`config` and related fields) is also defined by that author. Consult the handler's official documentation.

```javascript
// sfcc.ucp.shopperBusinessProfiles.v1.businessProfile.modifyGETResponse
var Status = require("dw/system/Status");

exports.modifyGETResponse = function (businessProfile) {
  businessProfile.ucp.paymentHandlers = {
    "com.google.pay": [
      {
        id: "...", // exact id from the handler author; do not invent one
        config: {
          /* see the handler author's spec */
        },
      },
    ],
  };
  return new Status(Status.OK);
};
```

The checkout mapper reads payment handlers from the business profile, so the handlers you assign here appear in the checkout session response without a separate hook.

#### Authorizing Payment

Register `sfcc.app.ucp.payment.authorize` to authorize the payment instrument with your PSP. The hook receives the `Order` and the `OrderPaymentInstrument`, and returns a `Status`: return `Status.OK` when authorization succeeds, or `Status.ERROR` to fail the order. Read any PSP tokens or handler data from the payment instrument's custom attributes (set earlier by your `buildPaymentInstrumentRequest` hook).

If you already have PSP authorization logic elsewhere, bridge to it from this hook rather than duplicating it:

- **SFRA merchants** call their existing `app.payment.processor.<METHOD>.Authorize` script.
- **Pipeline merchants** call their private `Authorize` start-node.
- **Merchants with an existing `dw.order.payment.authorize` script** delegate to it directly — the signature is the same `(Order, OrderPaymentInstrument)`.

**note:** **No storefront browser in UCP**

UCP checkout runs headless and server-side. There is no merchant-controlled browser, so any PSP logic that assumes client-side setup happened first (for example, a payment intent created in the shopper's browser) must be adapted to complete the full PSP interaction server-side inside the `authorize` hook.

#### Order Placement

Order placement is **synchronous** — it happens inside the `/complete` request. The platform places the order when both of these are true:

- Your `sfcc.app.ucp.payment.authorize` hook returns `Status.OK`.
- The order's _authorized_ amount covers its total.

At the start of `/complete`, the platform creates the order in `CREATED` status and runs your `authorize` hook. **Leave the payment transaction in its created state:** when the hook returns `Status.OK`, the platform promotes the transaction to _authorized_ and places the order for you. (You can set the transaction to _authorized_ yourself if you prefer, but it isn't required.)

If `authorize` returns `Status.ERROR` or throws, the platform fails the order, reopens the basket, and returns an error so the shopper can retry.

**note:** **Don't set the payment transaction to _captured_**

Placement requires the order's _authorized_ amount to cover its total, and a _captured_ transaction doesn't count toward that amount. So if your `authorize` hook captures the payment, the order is never placed — it stays in `CREATED` and is later failed by the `AutoFailOrders` job. Authorize during `/complete`; capture later (for example, at fulfillment).

#### Post-Placement Actions

A browser-based checkout finishes an order in the storefront controller — sending the confirmation email, exporting to an OMS, and so on. UCP checkout is headless, so that controller never runs. How you replace that work depends on the payment path:

- **Salesforce Payments:** use `dw.extensions.payments.sendOrderConfirmationEmail`, which runs after the gateway webhook places the order. Don't send the email from `complete.afterPOST` — the order is still `CREATED`. See [If You Use Salesforce Payments](#if-you-use-salesforce-payments-default).
- **Third-party (DIY):** `complete.afterPOST` receives the Order after `/complete` has placed it. Send the confirmation email there. Send only if the order is already placed (not `CREATED`). Don't return `ERROR` if the email send fails; log it, leave `confirmationStatus` unconfirmed, and let a job retry. A hook failure doesn't un-place the order, but it can still fail the `/complete` response. See [Transaction Management](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/ucp-transaction-management.md#the-complete-transaction-model).

**Recommendation:** whether or not you send the email in `complete.afterPOST`, run a scheduled job as a backstop for the email and for OMS export. Use the order's status attributes as an idempotent work queue — `confirmationStatus` for the email and `exportStatus` for OMS export. Guard each action by its own flag and commit each in its own transaction, so a failure in one action neither undoes the other nor re-fires it on the next run.

```javascript
// Custom job step: run post-placement actions for newly placed orders.
var OrderMgr = require("dw/order/OrderMgr");
var Order = require("dw/order/Order");
var Transaction = require("dw/system/Transaction");

exports.processPlacedOrders = function () {
  OrderMgr.processOrders(
    function (order) {
      if (order.confirmationStatus === Order.CONFIRMATION_STATUS_NOTCONFIRMED) {
        // Send the email server-side (for example, dw.net.Mail or a helper),
        // then mark the order confirmed so later runs skip it.
        Transaction.wrap(function () {
          order.setConfirmationStatus(Order.CONFIRMATION_STATUS_CONFIRMED);
        });
      }
      if (order.exportStatus === Order.EXPORT_STATUS_NOTEXPORTED) {
        // Export the order to your OMS, then mark it exported.
        Transaction.wrap(function () {
          order.setExportStatus(Order.EXPORT_STATUS_EXPORTED);
        });
      }
    },
    "status = {0} AND (confirmationStatus = {1} OR exportStatus = {2})",
    Order.ORDER_STATUS_OPEN,
    Order.CONFIRMATION_STATUS_NOTCONFIRMED,
    Order.EXPORT_STATUS_NOTEXPORTED,
  );
};
```

**note:** **Splitting into separate jobs**

Run the confirmation email and OMS export as two separate jobs instead if you prefer. If they run concurrently, be aware that B2C Commerce guards each order at the object level, not the field level: when both jobs touch the same order at the same moment, one transaction fails with a stale-data error even though they write different fields. Because the status flags make each update idempotent, the loser simply retries on its next run — or stagger the two schedules so they don't overlap. A single job doing all post-placement work avoids the contention entirely.
