Announcements
Configuration & Discovery
Checkout Session Authentication
Technical Audit
Implementation Considerations
Transaction Management
Enabling Google as a Platform
Connect a Third-Party OMS
B2C Commerce Release Notes
Ask the Community
The UCP Checkout Session APIs are Salesforce Commerce API (SCAPI) resources, not storefront controllers or OCAPI endpoints. That means when you extend the UCP Checkout Session APIs with hooks, your custom code runs inside a database transaction that the platform manages for you. Understanding where the transaction boundaries are — what is committed, what is rolled back, and where your hook runs relative to those boundaries — is essential to writing correct customizations for the create, get, update, complete, and cancel operations.
The Checkout Session APIs power headless checkout. Because the flow is headless, no storefront controller logic runs — only the server-side hooks described in this article execute. Your customizations must live in these hooks.
The transaction behavior described in this article is a platform implementation detail. Hook extension points and ordering reflect the current implementation and can change. This article covers B2C Commerce transaction management; it does not describe the checkout protocol itself.
Note
Like all SCAPI resources, each UCP state-changing request runs its system logic and hooks inside one platform-managed transaction — the standard contract described in Extensibility via Hooks. Two things make UCP different from a typical SCAPI resource, and both matter when you adapt existing hook code:
complete operation departs from the one-transaction-per-request model that the other operations follow (see The Complete Transaction Model).Transaction.wrap() behaves differently across channels. An SFRA storefront hook opens its own script transaction, and so can the Checkout Session create, update, and cancel hooks — but the complete flow already runs inside a platform-managed transaction where no script transaction is open, so calling Transaction.wrap() there can fail. Shared helper code must account for the channel it runs in (see Sharing Hook Code Between SFRA and Checkout Session APIs).Transaction management for Checkout Session hooks is a shared responsibility:
| Responsibility | Owner |
|---|---|
| Beginning, committing, and rolling back transactions | Salesforce (the platform) |
Returning a Status to signal success or failure | You |
| Correctness and performance of the code inside your hook | You |
| Placing external callouts in the correct hook | You |
You never call begin(), commit(), or rollback() yourself. Instead, you return a Status object, and the platform commits or rolls back the surrounding transaction based on that status.
For create, update, and cancel, the entire operation runs inside a single database transaction. The system logic that modifies the basket and the after hook both participate in that one transaction. Either everything commits, or nothing does.
This is the same contract documented for OCAPI and other SCAPI customizations: for state-changing HTTP methods like POST, PUT, PATCH, and DELETE, the server executes the after hook logic plus the system logic in the context of one database transaction. The transaction ensures that everything (or nothing) is committed into the database.
The processing flow for a create request:
| Processing Step | Comment |
|---|---|
Server receives the create request | The server begins the transaction. |
| Server applies system logic | The server creates the basket and applies line items, discounts, and defaults. |
Server calls the beforePOST hook | Your code can observe the basket before it is fully populated. Returning an ERROR status stops processing and rolls back the transaction. |
Server calls the afterPOST hook | Your code can adjust the basket. The platform runs the basket calculation here. Returning an ERROR status stops processing and rolls back the transaction. |
| Server commits the transaction | All basket changes from the steps above are persisted together. |
Server calls the modifyPOSTResponse hook | Runs after the commit, outside any transaction. Use it to shape the response document only. |
| Server sends the response | The server renders and returns the response document. |
The update operation follows the same pattern with beforePUT, afterPUT, and modifyPUTResponse. The get operation is a read and does not run in a state-changing transaction.
Don’t modify a Script API object in an HTTP GET request or a modifyResponse hook, because they aren’t executed in a transaction. Doing so can cause an ORMTransactionException and an HTTP 500 fault response. If you need to persist a change, make it in a before/after hook, complete.beforePOST, complete.afterPOST, or a payment hook instead.
Note
The complete operation combines several traditionally separate operations — order creation, payment authorization, and order placement — and therefore uses more than one transaction. Atomicity does not span the whole request. This is the most important difference to understand before you customize the complete flow.
| Transaction | Steps | Your hooks |
|---|---|---|
| Transaction 1 | Update billing address, build payment instruments, create the order from the basket | complete.beforePOST, payment.buildPaymentInstrumentRequest (DIY), payment.beforeOrderCreate |
| Transaction 2 | Authorize payment and place the order if fully authorized | payment.authorize (DIY only) |
| Transaction 3 (compensating) | Runs only if Transaction 2 fails: clean up the basket and fail the order, reopening the basket | — |
| After Transaction 2 | Own nested transaction after Transaction 2 has committed (order is placed on DIY; still CREATED on Salesforce Payments) | complete.afterPOST |
If Transaction 3 also fails, the scheduled Auto-Fail-Orders job is the backstop.
The consequences for your custom code:
complete.beforePOST returns ERROR) rolls back Transaction 1. No order is created and no payment is authorized. Because no external payment provider has been called yet, nothing is left stranded.complete.afterPOST does not run.authorize hook, which runs in Transaction 2. This ensures that a rollback of the order-creation transaction cannot leave a dangling authorization.complete.afterPOST runs after Transaction 2 has committed. A failure there does not un-place the order, but it can still fail the /complete HTTP response. See Post-Placement Actions.On the third-party (DIY) path, Transaction 2 places the order when authorize succeeds and the authorized amount covers the total.
On Salesforce Payments, Transaction 2 still runs and starts authorization, but the order is not placed in Transaction 2. The order stays CREATED until the gateway webhook arrives:
AUTHORISATIONpayment_intent.succeededDon’t treat a successful /complete response as a placed order on the Salesforce Payments path. complete.afterPOST still fires after /complete succeeds — with a CREATED order — so do not send the confirmation email from that hook on Salesforce Payments.
The hook catalog — grouped by create/get/update, cancel, complete, and payment — is in Extension Hooks Reference. This page covers only the transaction contract around those hooks.
Your hook controls the transaction through its return value, following the same contract as other SCAPI hooks:
OK status, the server continues processing and commits the transaction.ERROR status (a handled error), the server stops further processing and rolls back that hook’s transaction. For create, update, cancel, and complete Transactions 1 and 2, that rolls back the work in that transaction. For Checkout Session APIs, the error is surfaced in the checkout session response.complete.afterPOST is different: Transaction 2 has already committed. Returning ERROR or throwing does not un-place the order, but it can still fail the /complete HTTP response. Don’t return ERROR for a failed confirmation email; log it and let a job retry.1var Status = require('dw/system/Status');
2
3exports.afterPOST = function (basket) {
4 // Returning ERROR rolls back the transaction — no basket changes
5 // from this request are persisted.
6 if (shouldBlockCheckout(basket)) {
7 return new Status(Status.ERROR, 'MY_ERROR_CODE', 'Explain what went wrong.');
8 }
9
10 // Safe to modify the basket here; changes participate in the transaction.
11 // basket.custom.myAttribute = '...';
12
13 return new Status(Status.OK);
14};For each hook, your code should return a Status object. If your hook code doesn’t return a Status, multiple registered hook scripts might be executed, including the overridden base implementation, and one of their return values is used instead of yours.
Note
Do:
Status object from every hook.before/after hook (beforePOST, afterPOST, beforePUT, afterPUT), complete.beforePOST, complete.afterPOST, or a payment hook.authorize hook so that a rollback of Transaction 1 can’t strand them.Don’t:
modifyResponse hook or a get flow. It causes an ORMTransactionException and an HTTP 500 fault.CAPTURED inside the authorize hook.complete.afterPOST as a rollback point for placement, and don’t send the confirmation email from that hook on Salesforce Payments.Many implementations share helper scripts (for example, cart and pricing helpers) between an SFRA storefront and the Checkout Session API hooks. This introduces a subtle transaction-management difference that can cause runtime errors if you aren’t aware of it.
dw.system.Transaction.wrap() (and begin()) does not create a transaction on its own — it expects a script transaction to already be open and joins it. The available transaction context differs by execution channel:
| Execution context | Transaction context available to your script |
|---|---|
| SFRA / SiteGenesis controller | The request pipeline opens a script transaction for the request, so Transaction.wrap() works. |
| Checkout Session create, update, and cancel | A script transaction is open around the hook, so Transaction.wrap() works. |
| Checkout Session complete | The operation runs inside platform-managed transactions, but a script transaction is not open around every hook. Calling Transaction.wrap() from a hook in this flow (including dw.order.calculate, complete.beforePOST, complete.afterPOST, and the payment hooks) can fail with IllegalStateException: No transaction set for current thread. |
Because your persistent changes in the complete flow already run inside a platform-managed transaction, they are committed with the operation. Wrapping them again in Transaction.wrap() is unnecessary in that flow, and can cause the error above.
The Script API does not expose a way to test whether a transaction is currently open. To decide whether to wrap, detect the execution channel with dw.system.Request.isSCAPI() instead. This method returns true for Checkout Session API requests and false for SFRA storefront requests.
Note
To keep one shared helper working correctly in both channels, wrap only when the code is running outside a Checkout Session API request:
1var Transaction = require('dw/system/Transaction');
2
3function persistDerivedValues(lineItem, value) {
4 lineItem.custom.myDerivedValue = value;
5}
6
7if (request.isSCAPI()) {
8 // Checkout Session flow: already inside a platform-managed transaction — write directly.
9 persistDerivedValues(lineItem, value);
10} else {
11 // SFRA and other storefront entry points: open a script transaction.
12 Transaction.wrap(function () {
13 persistDerivedValues(lineItem, value);
14 });
15}Guidance for shared helpers:
request.isSCAPI(), rather than duplicating the logic.after hook rather than in a frequently-invoked hook like dw.order.calculate, which runs on every recalculation.request.isSCAPI() as a general “no transaction is open” signal outside of the checkout hook context. In a Custom API that does not open a transaction, a direct persistent write would still fail. Use it specifically to distinguish the SFRA and Checkout Session channels for a shared hook helper.Your hook code runs inside these transactions and, for modifyResponse hooks, on the response path. Its execution time is added directly to the API’s response time. Shopper APIs must respond in under 10 seconds, or an HTTP 504 timeout is returned and the shopper can’t complete the transaction.
authorize hook.modifyResponse hooks lightweight, because it runs on every uncached response.| Concern | Answer |
|---|---|
| Who manages the transaction? | The platform. Never begin, commit, or roll back in a hook. |
| Where does my code run? | Inside the operation’s transaction for before/after and payment hooks; in its own nested transaction for complete.afterPOST (after T2 has committed); outside a transaction for modifyResponse and get. |
| How do I abort and roll back? | Return an ERROR status, or throw. For complete.afterPOST, that does not un-place the order. |
| Are create, update, and cancel atomic? | Yes. One transaction covers the whole operation. |
| Is complete atomic? | No. It spans order-creation, authorization, and compensating fail-order transactions. |
Can I persist data in modifyResponse? | No. It’s non-transactional. Use a before/after, complete.beforePOST, complete.afterPOST, or payment hook. |
| Where do external payment callouts go? | The DIY authorize hook, which runs in Transaction 2. |
| When is the order placed? | DIY: Transaction 2. Salesforce Payments: later, on the gateway webhook. |