Creating an Order is the commit step of the checkout process. When an Order is created: - The referenced Cart's `cartState` changes to `Ordered`, 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 ```mermaid sequenceDiagram participant Client participant BFF participant Composable Commerce participant Subscriptions Client->>BFF: POST /checkout/place-order BFF->>Composable Commerce: POST /orders (OrderFromCartDraft: cartId + version) Composable Commerce-->>BFF: 201 Created (Order) BFF-->>Client: Confirmation payload Composable Commerce-->>Subscriptions: OrderCreated Message (via Subscription) Subscriptions-->>ERP: Sync order / fulfillment Subscriptions-->>Email: Send confirmation ``` # Preconditions before Order creation Before calling the Create Order from Cart endpoint, ensure the following preconditions are met: 1. The Cart has the following properties set: shipping address, shipping method, billing address (if required), and linked payments. 2. Payment authorization is complete for synchronous payment service provider (PSP) flows. 3. The latest Cart version is being used (to prevent stale data errors). 4. 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. ```ts 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: 1. Add a `CancelAuthorization` transaction to the existing Payment to record the cancellation. 2. When the PSP confirms the cancellation, update the transaction state to `Success` or `Failure`. 3. When the customer re-authorizes, create a new Payment object with `amountPlanned` set to the new Cart amount and link it to the Cart. ```ts // 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 To ensure idempotency when creating an Order, generate a unique `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. The `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. You can [create an Order](https://docs.commercetools.com/api/carts-orders-overview.md#create-an-order) from a Cart, a Quote, or by import. This section focuses on creating an Order from a Cart, which is the most common method for customer-facing checkouts. You can create Orders from [global](https://docs.commercetools.com/urn?urn=ctp%3Aapi%3Aendpoint%3A%2F%7BprojectKey%7D%2Forders%3APOST) or [Store-scoped](https://docs.commercetools.com/urn?urn=ctp%3Aapi%3Aendpoint%3A%2F%7BprojectKey%7D%2Fin-store%2Fkey%3D%7BstoreKey%7D%2Forders%3APOST) Carts. The Order will have the same scope as the Cart it is created from. To trigger post-order activities, such as analytics or system synchronization, you can use a Subscription. Creating an Order produces an [OrderCreated](https://docs.commercetools.com/api/projects/messages/cart-order-messages.md#order-created) Message, which can trigger these Subscriptions. ```ts // 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(); } ``` The `orderNumber` is an immutable, user-defined identifier. You can share it with customers to track their Order. If a server-side problem occurs during Order creation, indicated by a 5xx HTTP response, the Order might still have been created. In such cases, verify the Order's status by querying it using a unique identifier supplied in the creation request, such as the `orderNumber`. To know about specific error codes returned during Order creation, see the [Create Order from Cart](https://docs.commercetools.com/urn?urn=ctp%3Aapi%3Aendpoint%3A%2F%7BprojectKey%7D%2Forders%3APOST) endpoint. ## Example Subscription payload for OrderCreated When an Order is created, a Message with the following structure is generated, which can trigger a Subscription. ```json { "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 Cart `version` and deciding whether to retry or not. - Order identity: Supply a unique `orderNumber` for each Order. On a retry attempt, use the same `orderNumber` to avoid unnecessary generation. - Cart key: Set a unique `key` for 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 `Charge` transaction exists for a given PSP reference. If you are using multiple partial captures on a PSP that allows for this behavior—which results in multiple `Charge` transactions on the same Payment—the next pattern can be used instead. - Transaction Interaction ID: Ensure that every transaction has a unique `interactionId` across all transactions. In other words, ensure that multiple transactions do not have the same `interactionId` to avoid duplicating transactions. This strategy also covers the objectives of the previous strategy. `interactionId` is 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 `OrderCreated` Message. - 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.