submitCart we run some checks against the Cart to try and verify whether it can be successfully purchased, and if you passed us a payment method we'll charge it before moving to the next phase.submitCart returns, your order is placed on a queue and is processed asynchronously. You must poll the checkoutByCartID query for updates. Sometimes things go wrong at this stage and you'll need to replace the order.retryable, and order placement attaches this information to the OrderFailedOrderEvent object. In both cases, retryable is an optional boolean field which you can interpret like so:true value means our system believes the error is intermittent, and you can safely retry the operation.false value means the failure either isn't intermittent (and thus won't be fixed through retries), or that retrying the operation is unsafe (e.g. there is a risk of charging twice).retryable extension with value true, then that generally means the cart submission error needs to be fixed by either altering your request or coordinating with the merchant. For instance, the following response indicates that cart submission cannot be retried due to how the merchant has configured their store:1234567891011121314151617181920{ "data": { "submitCart": { "cart": { // ... "stores": [{ // ... "errors": [{ "code": "FEATURE_UNSUPPORTED", "message": "The merchant does not support promo codes." }] }] }, "errors": [] } }, "extensions": { "retryable": false } }
PAYMENT_FAILED) usually requires a change of payment method to resolve. You could prompt your shopper to key in a different credit card. Because we don't expect payment method errors to solve themselves with a retry, we'll send back retryable: false.UNDELIVERABLE) means that the merchant can't ship the order to the address in the buyer identity. You could prompt your shopper to enter a different delivery address. We'll send back retryable: false in this case.PRODUCTS_UNAVAILABLE) can sometimes resolve itself if the merchant happens to restock the product in between cart submission attempts. We'll send back retryable: true in this case.UPSTREAM_SERVICE_ERROR) is generally transient, and we'll usually send back retryable: true.submitCart involves making a synchronous request to our API. If you have configured a low request timeout—or our systems exceed our own internal timeout—you'll get back an HTTP 503 or HTTP 504 status code. In this case, it is entirely possible that your cart ended up successfully moving to the order placement phase in the background.submitCart. You must poll the checkoutByCartID query to verify what ended up happening to the cart after your request timed out. See the next section for specific guidance on this.checkoutByCartID query for updates on your order. Progress on each order is communicated via OrderEvent objects stored against each Order object on the Checkout.1234567891011121314query PollForCheckoutStatus($cartId: ID!) { checkoutByCartID(id: $cartId) { orders { events { __typename ... on OrderFailedOrderEvent { reason reasonCode retryable } } } } }
12345678910111213141516{ "data": { "checkoutByCartID": { "orders": [{ "events": [{ "__typename": "OrderSubmissionStartedOrderEvent" }, { "__typename": "OrderFailedOrderEvent", "reason": "An upstream provider is experiencing issues. Please try again later.", "reasonCode": "UPSTREAM_SERVICE_ERROR", "retryable": true }] }] } } }
submitCart timed out, and something prevented our system from moving it to the order placement phase. You can detect this by the events list being empty.submitCart on the original cart to retry submission.events list containing only a single event of type OrderSubmissionStartedOrderEvent.OrderFailedOrderEvent event inside the events list.OrderFailedOrderEvent.retryable field to determine whether you can automatically attempt to replace the order. The value of this field has the same meaning as the retryable extension.submitCart on the original Cart. Our systems prevent this, and you will receive a non-retryable ALREADY_SUBMITTED error code from the submitCart operation.Cart.submitCart when order placement fails. If you are an enterprise customer using invoice billing (and therefore do not pass us a payment method token), then the failed order will not be charged on your next invoice.async-retry NPM package to implement retries for the submitCart operation using exponential backoff.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158import retry from 'async-retry'; import { checkoutByCartId, createCart, submitCart, sleep } from './helpers'; const TEN_MINUTES_IN_MS = 1_000 * 60 * 10; async function createAndSubmitCartWithRetries(items, remainingRetries = 1) { const cartId = await createCart(items); const failedProductIds = []; const { result: submitResult, status: submitStatus } = await retry(async () => { const { result, status } = await submitCart(cartId); if (result.extensions.retryable) { throw new Error('Retry retryable error'); } return { result, status }; }, { retries: 3 }); const didSubmitTimeout = submitStatus === 503 || submitStatus === 504; if (!didSubmitTimeout) { if (submitResult.errors.length) { return { didSubmit: false, failedProductIds }; } for (const store of submitResult.cart.stores) { if (store.errors.length) { return { didSubmit: false, failedProductIds }; } } } const endPollingBy = Date.now() + TEN_MINUTES_IN_MS; const lineItemsByOrderId = new Map(); const processedOrderIds = new Set(); const retryableOrderIds = new Set(); while (Date.now() < endPollingBy) { const pollResult = await checkoutByCartId(cartId); for (const order of pollResult.data.orders) { lineItemsByOrderId.set(order.id, order.lineItems); } const didProcessAllOrders = processedOrderIds.size === pollResult.data.checkoutByCartID.orders.length; if (didProcessAllOrders) { break; } for (const order of pollResult.data.orders) { if (processedOrderIds.has(order.id)) { // Don't process the same order twice continue; } const orderFailedEvent = order.events.find((event) => ( event.__typename === 'OrderFailedOrderEvent', )); const orderSucceededEvent = order.events.find((event) => ( event.__typename === 'OrderSucceededOrderEvent', )); if (!orderFailedEvent && !orderSucceededEvent) { // Nothing to do yet continue; } if (orderSucceededEvent) { // Nothing to do -- all went OK processedOrderIds.add(order.id); } else if (orderFailedEvent) { // Track items belonging to this order so we can either retry with a new // cart or communicate with the user later. processedOrderIds.add(order.id); if (orderFailedEvent.retryable) { retryableOrderIds.add(order.id); } else { // Track failed products we cannot retry if (lineItem.__typename === 'AmazonLineItem') { failedProductIds.push({ marketplace: 'AMAZON', productId: lineItem.productId, }); } else { failedProductIds.push({ marketplace: 'SHOPIFY', variantId: lineItem.variantId, }); } } } else { // No info about this order; chill. } } // Sleep between polls to be a good citizen await sleep(200); } for (const [orderId, lineItems] of lineItemsByOrderId.entries()) { if (!processedOrderIds.has(orderId)) { // This order got stuck. We'll add it to our list of retryable order IDs. retryableOrderIds.add(orderId); } } // Do we have retryable order IDs and remaining retries? if (retryableOrderIds.size && remainingRetries > 0) { const retryableCartItems = { amazonCartItemsInput: [], shopifyCartItemsInput: [], }; // Assemble cart items for new cart for (const [orderId, lineItems] of lineItemsByOrderId.entries()) { if (!retryableOrderIds.has(orderId)) continue; for (const lineItem of lineItems) { if (lineItem.__typename === 'AmazonLineItem') { retryableCartItems.amazonCartItemsInput.push({ productId: lineItem.productId, quantity: lineItem.quantity, }); } else { retryableCartItems.shopifyCartItemsInput.push({ quantity: lineItem.quantity, variantId: lineItem.variantId, }); } } } // Retry products w/ a new cart const { failedProductIds: failedRetriedProductIds } = await createAndSubmitCartWithRetries( retryableCartItems, remainingRetries - 1, ); failedProductIds.push(...failedRetriedProductIds); } return { didSubmit: true, failedProductIds }; } const result = await createAndSubmitCartWithRetries({ shopifyCartItemsInput: [{ quantity: 1, variantId: '44346795295022', }], }); if (!result.didSubmit) { // TODO: Inform user checkout did not even start (e.g. payment error). } else if (result.failedProductIds.length) { // TODO: Inform user about failed checkout for some products } else { // TODO: Inform user about checkout success }