Order creation

Learn how to programmatically create an Order and handle post-order actions in Composable Commerce.

Copy for LLM
View as Markdown

After completing this page, you should be able to:

  • Create an Order from a Cart after validating preconditions and ensuring payment authorization.

  • Implement idempotency strategies to prevent duplicate orders on retries.

  • Handle practical scenarios like recording PSP transaction outcomes and managing Cart total changes.

  • Use Subscriptions to trigger post-order actions like sending confirmation emails or syncing with an ERP.

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

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.

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.
// 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 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 or Store-scoped 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 Message, which can trigger these Subscriptions.
// 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 endpoint.

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

Test your knowledge