Implementation Considerations

This section covers the UCP extensibility architecture and provides guidance for building custom hook implementations when needed. If you are still deciding whether you need custom work, start with the Technical Audit.

UCP Extensibility Architecture 

UCP follows the standard SCAPI extensibility with Hooks pattern, including hook script execution.

Core concept: A UCP Checkout Session is a headless “view” on a standard Commerce Cloud Basket. Standard Basket and Order hooks (calculate, calculateTax, calculateShipping, validateBasket, createOrderNo) run automatically. UCP is not 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) do not run. Remap that logic onto the UCP checkout-session hooks below. The Technical Audit has the inventory checklist.

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 do not run and a shopper cannot complete a checkout session — custom third-party payment hooks will not run either. Enable it before you test or go live.

Note

Extensibility Models 

The following diagrams illustrate 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

PUT /checkout-sessions/{id} - Update Checkout Session 

Updates/replaces products, buyer information, fulfillment, etc. on the underlying basket.

PUT checkout-sessions extensibility model

GET /checkout-sessions/{id} - Get Checkout Session 

Returns the current basket state as a checkout session.

GET checkout-sessions extensibility model

POST /checkout-sessions/{id}/cancel - Cancel Checkout Session 

Cancel the checkout session, delete the basket (terminal).

POST cancel checkout-sessions extensibility model

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 before you put callouts or Transaction.wrap() in complete-flow hooks.

Complete Transaction 1

POST complete checkout-sessions t1 extensibility model

Complete Transaction 2 - SFP (Default)

POST complete checkout-sessions sfp t2 extensibility model

Complete Transaction 2 - 3PP

POST complete checkout-sessions 3pp t2 extensibility model

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.

Adyen notification async flow

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 — you can persist Script API changes (basket or order). Returning ERROR rolls back that transaction.
  • No — the hook is not 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 does not 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.

For the UCP Checkout schema structure, see the Checkout reference. B2C Commerce 26.9 also serves protocol version 2026-01-23; see Protocol versions.

Standard Basket and Order Hooks 

These run automatically on checkout-session mutations. They are not UCP-specific extension points.

HookArgumentsTransactionalTypical Use
dw.order.calculateScript BasketYesPricing and basket totals
dw.order.calculateTaxScript BasketYesTax calculation
dw.order.calculateShippingScript BasketYesShipping calculation
dw.ocapi.shop.basket.validateBasketSCAPI Basket, onComplete: Boolean (true if called on /complete)YesAdd or remove flashes that drive checkout status
dw.order.createOrderNoYesCustom order number; runs during complete

Lifecycle Hooks: Create, Get, and Update 

Extension point prefix: sfcc.ucp.shopperCheckouts.v1.checkoutSessions

OperationHookTransactionalTypical Use
CreatebeforePOST(basket, request)YesObserve or adjust the basket before it is fully populated
CreateafterPOST(basket)YesAdjust the basket; the platform runs calculation here
CreatemodifyPOSTResponse(response)NoShape the response document only
GetmodifyGETResponse(response)NoShape the response document only
UpdatebeforePUT(basket, request)YesObserve or adjust the basket before the replace
UpdateafterPUT(basket)YesAdjust the basket; the platform runs calculation here
UpdatemodifyPUTResponse(response)NoShape the response document only

basket is a Script Basket. request and response are the UCP Checkout document.

Cancel Hooks 

Extension point prefix: sfcc.ucp.shopperCheckouts.v1.checkoutSessions.cancel

HookTransactionalTypical Use
afterPOST(checkoutSessionId)YesReact to the cancellation; the session is being deleted
modifyPOSTResponse(response)NoShape the response document only

Complete Hooks 

Extension point prefix: sfcc.ucp.shopperCheckouts.v1.checkoutSessions.complete

HookTransactionalTypical Use
beforePOST(basket, request)YesFinal validation or basket adjustment before order creation
afterPOST(order)YesPost-placement work on the Order (for example DIY confirmation email). Does not roll back the order.
modifyPOSTResponse(response)NoShape the response document only

complete.afterPOST receives a Script Order. See 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. Salesforce Payments–only hooks are under If You Use Salesforce Payments.

HookTransactionalTypical Use
beforeOrderCreate(basket)YesLast chance to adjust the basket after payment instruments are built and before createOrder
buildPaymentInstrumentRequest(basket, selectedInstrument, requestWO)YesTranslate the selected instrument into a payment instrument request (DIY)
authorize(order, paymentInstrument)YesAuthorize payment with a third-party payment service provider (DIY)

Business Profile and Order Webhooks 

HookTransactionalTypical Use
sfcc.ucp.shopperBusinessProfiles.v1.businessProfile.modifyGETResponse(response)NoReplace payment_handlers on the well-known profile (DIY). Do not change ucp.version.
sfcc.ucp.orderWebhook.v1.modifyEventPayload(payload)NoMutate the signed lifecycle payload immediately before delivery to the platform

If modifyEventPayload fails or returns a non-OK status, the webhook is retried.

UCP Checkout Session Validation 

By default, Checkout Session APIs perform basic validations for:

  • Buyer email
  • Line items
  • Shipping address
  • Shipping method

Additionally, on checkout session complete:

  • Billing address
  • Payment method

Adding and Removing Validation Errors 

Add, edit, or remove validation errors in dw.ocapi.shop.basket.validateBasket.

Use UCP standard error codes for the type field to ensure consistent error handling across platforms.

Note

1exports.validateBasket = function (basketResponse, duringSubmit) {
2    // Add a validation error
3    basketResponse.addFlash({
4        type: "eligibility_invalid",
5        message: Resource.msg('error.age.verification', 'checkout', null),
6        path: "$"
7    });
8
9    // Remove a validation error
10    var toRemove = [];
11    for each (f in basketResponse.flashes) {
12        if (f.type === "InvalidBillingAddress") {
13            toRemove.push(f);
14        }
15    }
16    for (var i = 0; i < toRemove.length; i++) {
17        basketResponse.removeFlash(toRemove[i]);
18    }
19}

Flash to Message Mapping 

Salesforce automatically maps Flashes to UCP Messages:

Flash PropertyUCP Message Property
flash.typemessage.code
flash.pathmessage.path
flash.messagemessage.content

The following 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 the following 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 

This example shows how to add custom address verification using a third-party service (Loqate) in dw.ocapi.shop.basket.validateBasket.

1// Hook: dw.ocapi.shop.basket.validateBasket
2exports.validateBasket = function (basketResponse, duringSubmit) {
3  // Validate shipping address with Loqate
4  var address = basketResponse.shipments[0].shippingAddress;
5  if (address) {
6    var addressForm = {
7      Address1: address.address1,
8      Address2: address.address2,
9      Country: address.countryCode,
10      State: address.stateCode,
11      City: address.city,
12      PostalCode: address.postalCode,
13    };
14    var response = loqate.validateLoqateAddress(addressForm);
15    if (response && response.response) {
16      basketResponse.addFlash({
17        type: "address_undeliverable",
18        message: "Item(s) in your cart can't be shipped to the selected address",
19        path: "$.shipments[0].shippingAddress",
20      });
21    }
22  }
23};

Filtering Store Pickup Fulfillment Methods 

Use the modifyResponse hooks to filter out store pickup fulfillment methods when the platform does not support store pickup.

Example: Do not show store pickup fulfillment methods

1// sfcc.ucp.shopperCheckouts.v1.checkoutSessions.modifyPOSTResponse
2exports.modifyPOSTResponse = function( checkoutSession )
3{
4    var pickupMethodIds = {};
5    for each( m in ShippingMgr.getAllShippingMethods() ) {
6        if ( m.custom.ucpStorePickupEnabled === true )
7        {
8            pickupMethodIds[m.ID] = true;
9        }
10    }
11
12    for each( method in checkoutSession.fulfillment.methods ) {
13        for each( group in method.groups ) {
14            var filtered = [];
15            for each( option in group.options ) {
16                if ( !pickupMethodIds[option.id] )
17                {
18                    filtered.push( option );
19                }
20            }
21            group.options = filtered;
22        }
23    }
24}

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 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.

The Payment Provider setting selects which path applies. See 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.

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 do not fire for third-party (DIY) payment.

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.

Note

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.

The code snippets here are for a third-party integration using the Adyen-managed adyen-salesforce-commerce-cloud cartridge. Your own implementation may vary, but the same principles apply.

Note

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)

1createAdyenRequestObject(orderNo, orderToken, paymentInstrument, customerEmail) {
2    // ...
3    stateData.applicationInfo = adyenHelperObj.getApplicationInfo();
4    stateData.additionalData = {};
5
6    // Tag payments created by this cartridge so webhook notifications
7    // can be identified. Adyen echoes this metadata back in webhook
8    // additionalData as "metadata.<key>".
9    stateData.metadata = {
10        paymentSource: 'adyen-sfcc-cartridge'
11    };
12
13    return stateData;
14}
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.

1function notify(req, res, next) {
2  // ...
3  var source = req.form["additionalData.metadata.paymentSource"];
4  if (source !== "adyen-sfcc-cartridge") {
5    // This payment wasn't created from this cartridge
6    // Return [accepted] and do no processing
7    res.render("/notify");
8    return next();
9  }
10
11  // This payment had our metadata => was created by this cartridge
12  // Continue with typical webhook processing flow
13  Transaction.begin();
14  // ...
15}

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 does not assemble the Salesforce Payments handler. See Payment Provider Setup.

Third-Party Payment Hooks 

Register these three hooks. Signatures are in 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 & Discovery. The hook argument exposes ucp and signingKeys. Assign handlers on ucp.paymentHandlers. Do not 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.

1// sfcc.ucp.shopperBusinessProfiles.v1.businessProfile.modifyGETResponse
2var Status = require('dw/system/Status');
3
4exports.modifyGETResponse = function (businessProfile) {
5    businessProfile.ucp.paymentHandlers = {
6        'com.google.pay': [
7            {
8                id: '...', // exact id from the handler author; do not invent one
9                config: { /* see the handler author's spec */ }
10            }
11        ]
12    };
13    return new Status(Status.OK);
14};

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, you can bridge to it from this hook rather than duplicating it:

  • SFRA merchants can call their existing app.payment.processor.<METHOD>.Authorize script.
  • Pipeline merchants can call their private Authorize start-node.
  • Merchants with an existing dw.order.payment.authorize script can delegate to it directly — the signature is the same (Order, OrderPaymentInstrument).

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.

Note

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 you don’t need to.)

If authorize returns Status.ERROR or throws, the platform fails the order, reopens the basket, and returns an error so the shopper can retry.

Don’t set the payment transaction to captured

Placement requires the order’s authorized amount to cover its total, and a captured transaction does not 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).

Note

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. Do not send the email from complete.afterPOST — the order is still CREATED. See If You Use Salesforce Payments.
  • Third-party (DIY): complete.afterPOST receives the Order after /complete has placed it. You can 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 does not un-place the order, but it can still fail the /complete response. See Transaction Management.

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.

1// Custom job step: run post-placement actions for newly placed orders.
2var OrderMgr = require('dw/order/OrderMgr');
3var Order = require('dw/order/Order');
4var Transaction = require('dw/system/Transaction');
5
6exports.processPlacedOrders = function () {
7    OrderMgr.processOrders(function (order) {
8        if (order.confirmationStatus === Order.CONFIRMATION_STATUS_NOTCONFIRMED) {
9            // Send the email server-side (for example, dw.net.Mail or a helper),
10            // then mark the order confirmed so later runs skip it.
11            Transaction.wrap(function () {
12                order.setConfirmationStatus(Order.CONFIRMATION_STATUS_CONFIRMED);
13            });
14        }
15        if (order.exportStatus === Order.EXPORT_STATUS_NOTEXPORTED) {
16            // Export the order to your OMS, then mark it exported.
17            Transaction.wrap(function () {
18                order.setExportStatus(Order.EXPORT_STATUS_EXPORTED);
19            });
20        }
21    }, 'status = {0} AND (confirmationStatus = {1} OR exportStatus = {2})',
22       Order.ORDER_STATUS_OPEN,
23       Order.CONFIRMATION_STATUS_NOTCONFIRMED,
24       Order.EXPORT_STATUS_NOTEXPORTED);
25};

Splitting into separate jobs

You can run the confirmation email and OMS export as two separate jobs instead. 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.

Note