DEV Community

Cover image for Calculate average order value from Shopify orders with the Admin GraphQL API
Brix
Brix

Posted on Originally published at thebrix.io

Calculate average order value from Shopify orders with the Admin GraphQL API

Disclosure: I work on Brix, a Shopify cart app. This post is about the maths and the API, and Brix is mentioned only at the end.

Average order value (AOV) is total revenue divided by the number of orders. The formula is one line. Getting a number you can trust takes a bit more care, because the totals Shopify gives you include things you may not want to count.

In this post we will pull orders with the Admin GraphQL API and compute AOV in Node.js, while handling the usual traps: test orders, cancelled orders, refunds, shipping and taxes.

What you need

  • A Shopify store, ideally a development store to try this on.
  • A custom app with an Admin API access token and the read_orders scope. By default this covers the last 60 days of orders. For older history you need the read_all_orders scope, which Shopify has to approve.
  • Node.js 18 or newer, which includes fetch.

Keep the token out of your code. We will read it from environment variables.

The query

We ask for orders in a date range and take only the fields we need. currentTotalPriceSet and currentSubtotalPriceSet reflect the order as it stands now, so refunds and returns are already taken into account.

query Orders($query: String!, $cursor: String) {
  orders(first: 250, after: $cursor, query: $query) {
    pageInfo {
      hasNextPage
      endCursor
    }
    nodes {
      id
      test
      cancelledAt
      currentTotalPriceSet { shopMoney { amount currencyCode } }
      currentSubtotalPriceSet { shopMoney { amount } }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • currentTotalPriceSet is the order total, including shipping and taxes.
  • currentSubtotalPriceSet is the product subtotal after discounts, without shipping and taxes.
  • test and cancelledAt let us drop orders that should not count.

The script

const SHOP = process.env.SHOPIFY_STORE;        // e.g. my-store.myshopify.com
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN; // Admin API access token
const API_VERSION = "2026-07";                 // use a current stable version

const QUERY = `
  query Orders($query: String!, $cursor: String) {
    orders(first: 250, after: $cursor, query: $query) {
      pageInfo { hasNextPage endCursor }
      nodes {
        id
        test
        cancelledAt
        currentTotalPriceSet { shopMoney { amount currencyCode } }
        currentSubtotalPriceSet { shopMoney { amount } }
      }
    }
  }
`;

async function graphql(variables) {
  const res = await fetch(
    `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Shopify-Access-Token": TOKEN,
      },
      body: JSON.stringify({ query: QUERY, variables }),
    }
  );
  const json = await res.json();
  if (json.errors) throw new Error(JSON.stringify(json.errors));
  return json.data.orders;
}

async function getOrders(from, to) {
  const search = `created_at:>=${from} created_at:<${to}`;
  const orders = [];
  let cursor = null;
  let hasNext = true;

  while (hasNext) {
    const page = await graphql({ query: search, cursor });
    orders.push(...page.nodes);
    hasNext = page.pageInfo.hasNextPage;
    cursor = page.pageInfo.endCursor;
  }
  return orders;
}

function aov(orders) {
  // Leave out test orders and cancelled orders.
  const real = orders.filter((o) => !o.test && !o.cancelledAt);
  if (real.length === 0) return null;

  const sum = (pick) =>
    real.reduce((total, o) => total + Number(pick(o)), 0);

  const revenue = sum((o) => o.currentTotalPriceSet.shopMoney.amount);
  const products = sum((o) => o.currentSubtotalPriceSet.shopMoney.amount);

  return {
    orders: real.length,
    currency: real[0].currentTotalPriceSet.shopMoney.currencyCode,
    aovTotal: revenue / real.length,    // includes shipping and tax
    aovProducts: products / real.length // product value only
  };
}

(async () => {
  const orders = await getOrders("2026-09-01", "2026-10-01");
  console.log(aov(orders));
})();
Enter fullscreen mode Exit fullscreen mode

Run it with:

SHOPIFY_STORE=my-store.myshopify.com SHOPIFY_ADMIN_TOKEN=shpat_xxx node aov.js
Enter fullscreen mode Exit fullscreen mode

The output looks like this (the numbers here are only an example of the shape):

{ orders: 412, currency: "USD", aovTotal: 71.3, aovProducts: 62.9 }
Enter fullscreen mode Exit fullscreen mode

Why two numbers

I return both on purpose, because teams often compare AOV across months using different definitions without noticing.

  • aovTotal includes shipping and taxes. It is closest to what the customer paid.
  • aovProducts is product value after discounts. It is better for judging whether upsells and bundles are working, because shipping and tax changes will not move it.

Pick one and stay with it. If one month uses the total and the next uses the product value, the comparison tells you nothing.

Traps to watch for

  1. Test orders. Orders placed while you test checkout show test: true. Leave them out.
  2. Cancelled orders. They have a cancelledAt value. Leave them out too.
  3. Refunds and returns. Using the current... fields means refunds are already reflected. If you use the original totalPriceSet instead, a refunded order still counts in full.
  4. Outliers. One very large wholesale order can move the average on a small store. Look at the median alongside the mean.
  5. Currency. shopMoney is in the shop's currency. If you sell in several presentment currencies, do not mix them in one sum.
  6. Date ranges. Use the same length of period when you compare, and watch out for sales and holidays.

Read it next to other numbers

AOV on its own can mislead. A change that raises AOV but lowers conversion rate can leave revenue flat. Look at AOV with conversion rate, orders per day and margin.

I wrote a longer, non-code version of this guide for store owners, covering where to find AOV in the Shopify admin and how to track it over time: How to calculate and track average order value on Shopify.

If you are working on raising AOV and not only measuring it, Brix is the Shopify cart app I work on (upsells, rewards, coupons and bundles). Questions about the query or the maths are welcome in the comments.

Top comments (0)