Creating an Order is the commit step of the checkout process. When an Order is created:
- The referenced Cart's
cartStatechanges toOrdered, and the Cart can no longer be modified. - All prices, discounts, taxes, shipping, and payment details are snapshotted and captured on the Order.
- Downstream processes such as ERP synchronization, email confirmation, and inventory updates are triggered.
Order creation can fail if a validation fails at the last moment, such as a tax rate change or an invalid discount. Your implementation must provide a clear recovery path for the customer.
A robust implementation ensures:
- Idempotency: Prevents duplicate orders on retries.
- Error handling: Provides recoverable failures with user-friendly messaging.
- Security: Ensures only authorized users can place the order.
Order placement sequence
Preconditions before Order creation
Before calling the Create Order from Cart endpoint, ensure the following preconditions are met:
- The Cart has the following properties set: shipping address, shipping method, billing address (if required), and linked payments.
- Payment authorization is complete for synchronous payment service provider (PSP) flows.
- The latest Cart version is being used (to prevent stale data errors).
- All business validations, such as minimum order value or stock checks, have passed.
Practical patterns
The following code examples demonstrate how to perform common actions required before creating an Order.
Record PSP outcomes as Transactions
Record the outcome of a payment service provider (PSP) interaction by adding a Transaction to the Payment. This creates an audit trail and is essential for handling refunds, chargebacks, and reconciliations.
await apiRoot
.payments()
.withId({ ID: paymentId })
.post({
body: {
version,
actions: [
{
action: "addTransaction",
transaction: {
type: "Charge",
amount: { centAmount: amount, currencyCode },
state: "Success",
interactionId: pspChargeId,
timestamp: new Date().toISOString(),
},
},
],
},
})
.execute();
Handle Cart totals change
If the Cart total changes, creating a discrepancy between the Cart total and the authorized Payment amount, you must handle this situation according to your business rules.
A common practice is to cancel the original authorization and require the customer to re-authorize the payment for the updated amount—it helps avoid disputes and keeps transaction records consistent. This "cancel and restart" approach guarantees that the payment authorization matches the final purchase amount exactly. However, this can add friction to the checkout experience and increase the risk of cart abandonment.
When following the above-mentioned approach:
- Add a
CancelAuthorizationtransaction to the existing Payment to record the cancellation. - When the PSP confirms the cancellation, update the transaction state to
SuccessorFailure. - When the customer re-authorizes, create a new Payment object with
amountPlannedset to the new Cart amount and link it to the Cart.
// Cancel authorization on PSP
// Implement according to your PSP-specific API
const { pspCancellationId, status } = await psp.cancelAuthorization(originalPayment.interfaceId);
// Record cancellation transaction
await apiRoot
.payments()
.withId({ ID: originalPayment.id })
.post({
body: {
version: originalPayment.version,
actions: [
{
action: "addTransaction",
transaction: {
type: "CancelAuthorization",
amount: originalPayment.amountPlanned,
state: status === "success" ? "Success" : "Pending",
interactionId: pspCancellationId,
timestamp: new Date().toISOString(),
},
},
],
},
})
.execute();
Generate a unique Order number
orderNumber before calling the Create Order endpoint. The orderNumber is a user-defined identifier that must be unique within a Project; otherwise, an error is returned when calling the endpoint. This ensures you to safely retry Order creation without creating duplicate Orders.orderNumber also serves as a reliable reference for customers and for integrating with external systems like an ERP or CRM.Create Order from Cart
Once all preconditions are met, you can create the Order.
// For Store-scoped Carts (Carts associated with a Store)
async function createOrderFromCartOnStore(
storeKey: string,
cartId: string,
cartVersion: number
) {
return apiRoot
.inStoreKeyWithStoreKeyValue({ storeKey })
.orders()
.post({
body: {
// Order from Cart
cart: { typeId: "cart", id: cartId },
version: cartVersion,
orderNumber, // Optional: your unique human-friendly ID
},
})
.execute();
}
// For Global Carts (Carts not associated with a Store)
async function createOrderFromCart(cartId: string, cartVersion: number) {
return apiRoot
.orders()
.post({
body: {
// Order from Cart
cart: { typeId: "cart", id: cartId },
version: cartVersion,
orderNumber, // Optional: your unique human-friendly ID
},
})
.execute();
}
orderNumber is an immutable, user-defined identifier. You can share it with customers to track their Order.orderNumber.Example Subscription payload for OrderCreated
When an Order is created, a Message with the following structure is generated, which can trigger a Subscription.
{
"notificationType": "Message",
"message": {
"type": "OrderCreated",
"order": {
"id": "12345",
"orderNumber": "ABC-123",
"totalPrice": { "currencyCode": "USD", "centAmount": 5000 }
}
}
}
Your listener service can use this payload to send a confirmation email, notify the warehouse system, or trigger a fraud analysis.
Idempotency strategies
-
Cart versioning: Order creation requires the latest Cart
version. A retry with an old version will fail, preventing duplicate Orders. You can handle this error by refetching the latest Cartversionand deciding whether to retry or not. -
Order identity: Supply a unique
orderNumberfor each Order. On a retry attempt, use the sameorderNumberto avoid unnecessary generation. -
Cart key: Set a unique
keyfor each Cart before creating the Order and use it in your deduplication logic. -
Payment transaction ID: If you are using single captures (entire amount captured in one action), to prevent duplicate charges, ensure only one
Chargetransaction exists for a given PSP reference. If you are using multiple partial captures on a PSP that allows for this behavior—which results in multipleChargetransactions on the same Payment—the next pattern can be used instead. -
Transaction Interaction ID: Ensure that every transaction has a unique
interactionIdacross all transactions. In other words, ensure that multiple transactions do not have the sameinteractionIdto avoid duplicating transactions. This strategy also covers the objectives of the previous strategy.interactionIdis an identifier used by the PSP to reference a specific transaction (this includes an authorization or charge). It is unique for every payment operation.
Post-Order actions
After an Order is created:
- Send a confirmation email: Use an external service like SendGrid or AWS SES, triggered by a Subscription on the
OrderCreatedMessage. - Sync with ERP/Fulfillment: Trigger synchronization with downstream systems via a Subscription or integration middleware.
Error handling in Order creation
The following are some recovery strategies for some of the common errors that might occur:
- Concurrent modification (wrong Cart version): Refetch the Cart to get the latest version and data. Cancel previous authorization (or refund), update the Payment object, and present the updated totals to the customer for confirmation before creating a new Payment object and retrying the Order creation.
- Inventory shortage: Inform the customer that an item is out of stock, and allow them to adjust quantities or remove the items before retrying.
- Price, tax, or discount change: Display the updated order summary to the customer and require them to confirm the new totals before proceeding.
- Payment pending: If a payment is pending, poll the PSP for a status update or await a webhook notification before creating the Order.
Architect considerations
- Partial payment scenarios: Your design must handle multiple Payment objects on a single Cart, such as when a gift card and a credit card are used together.
- Event-driven post-processing: Use Subscriptions to trigger downstream processes. This offloads tasks like sending emails or syncing with an ERP to separate, asynchronous workers while keeping your BFF or primary backend service lightweight.
Key takeaways
- Validate all preconditions before attempting to create an Order to minimize late-stage failures.
- Implement idempotency at multiple levels (Cart, Order, Payment) to prevent duplicate orders and charges on retries.
- Use Subscriptions to decouple post-order processes like email notifications and ERP synchronization, keeping your checkout flow lean and resilient.
- Design for asynchronous payment flows to build a robust system that can handle payment delays or manual reviews.
- Keep the BFF lightweight by offloading long-running or non-critical post-order tasks to separate, event-driven workers.
Next, we will discuss the best practices, where we consolidate security, idempotency, testing, and extensibility guidelines for developers and architects.