Paid Shopify-to-Sheets connectors usually cost $10–30 a month. If all you need is a running list of orders for reporting, a short Google Apps Script does the same job for free. Here is the whole setup.
What you’ll end up with
- A tab called Orders with one row per order: order number, date, payment status, fulfillment status, item count, total and currency.
- New orders added on their own every hour, even when your computer is off.
- No duplicates: the script skips orders that are already in the sheet.
Step 1: Get a Shopify Admin API access token
The script needs read-only access to your orders. In Shopify this comes from a custom app that you create for your own store:
- Create a custom app for your store. On most stores this is under Settings → Apps → Develop apps; Shopify moves this screen from time to time, so if you don’t see it, search their help center for “custom apps”.
- Give it one Admin API scope:
read_orders. Nothing else is needed. - Install the app and copy the Admin API access token (it starts with
shpat_). Treat it like a password.
Note: read_orders covers the last 60 days of orders. For older history Shopify also requires the read_all_orders scope, which you request from Shopify separately.
Step 2: Add the script to your sheet
- Create a new Google Sheet and open Extensions → Apps Script.
- Delete the sample code and paste in the script below.
- Change
SHOPto your store’s.myshopify.comaddress. - Open Project Settings (the gear icon), scroll to Script properties, and add a property named
SHOPIFY_TOKENwith your token as the value. Keeping it here rather than in the code means it won’t leak if you share the script.
const SHOP = 'your-store.myshopify.com';
const API_VERSION = '2026-07'; // any version Shopify still supports
const SHEET_NAME = 'Orders';
const HEADERS = ['Order', 'Created', 'Payment', 'Fulfillment', 'Items', 'Total', 'Currency'];
const QUERY = `
query ($cursor: String, $q: String) {
orders(first: 100, after: $cursor, query: $q, sortKey: CREATED_AT) {
pageInfo { hasNextPage endCursor }
nodes {
name
createdAt
displayFinancialStatus
displayFulfillmentStatus
currentSubtotalLineItemsQuantity
totalPriceSet { shopMoney { amount currencyCode } }
}
}
}`;
function syncOrders() {
const props = PropertiesService.getScriptProperties();
const token = props.getProperty('SHOPIFY_TOKEN');
if (!token) throw new Error('Add SHOPIFY_TOKEN under Project Settings > Script properties');
const sheet = getSheet_();
const existing = new Set(sheet.getRange('A2:A').getValues().flat().filter(String));
const since = props.getProperty('LAST_ORDER_AT') || '2000-01-01T00:00:00Z';
const rows = [];
let newest = since;
let cursor = null;
do {
const data = shopify_(token, { cursor: cursor, q: `created_at:>='${since}'` });
const page = data.orders;
page.nodes.forEach(function (o) {
if (o.createdAt > newest) newest = o.createdAt;
if (existing.has(o.name)) return;
rows.push([
o.name,
new Date(o.createdAt),
o.displayFinancialStatus,
o.displayFulfillmentStatus,
o.currentSubtotalLineItemsQuantity,
Number(o.totalPriceSet.shopMoney.amount),
o.totalPriceSet.shopMoney.currencyCode,
]);
});
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
} while (cursor);
if (rows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, HEADERS.length).setValues(rows);
}
props.setProperty('LAST_ORDER_AT', newest);
}
function shopify_(token, variables) {
const res = UrlFetchApp.fetch(`https://${SHOP}/admin/api/${API_VERSION}/graphql.json`, {
method: 'post',
contentType: 'application/json',
headers: { 'X-Shopify-Access-Token': token },
payload: JSON.stringify({ query: QUERY, variables: variables }),
muteHttpExceptions: true,
});
const body = JSON.parse(res.getContentText());
if (res.getResponseCode() !== 200 || body.errors) {
throw new Error('Shopify error: ' + res.getContentText().slice(0, 500));
}
return body.data;
}
function getSheet_() {
const ss = SpreadsheetApp.getActive();
let sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
sheet.appendRow(HEADERS);
sheet.setFrozenRows(1);
}
return sheet;
}
Step 3: Run it once
Pick syncOrders in the function menu at the top and click Run. Google will ask you to authorize the script; it needs permission to edit this spreadsheet and to connect to an external service (Shopify). After a few seconds your orders appear in the Orders tab.
The script remembers the newest order it has seen, so each later run only asks Shopify for recent orders and stays fast.
Step 4: Make it run every hour
- In Apps Script, click the clock icon (Triggers) and then Add Trigger.
- Choose the function
syncOrders, event source Time-driven, type Hour timer, every hour. - Save. Google now runs the sync for you and emails you if a run fails.
Common problems
- “Access denied” or 401: the token is wrong, or the app wasn’t given
read_orders. Re-check the scope and reinstall the app. - Only recent orders show up: that’s the 60-day limit of
read_orders(see Step 1). - An old API version error: Shopify retires versions after about a year. Change
API_VERSIONto a newer one from Shopify’s API docs. - You want customer names, line items or refunds too: add the fields to the query. Customer details need extra permissions from Shopify, which is why they’re left out here.
Taking it further
The same pattern works for any service with an API: Stripe payments, WooCommerce orders, ad spend or app data. Once the rows are in Sheets you can add a pivot table, a monthly summary tab, or an email report that goes out every Monday.
Want this set up for you?
I build these syncs for Shopify, Stripe and other APIs, tested on a copy of your sheet, with a short guide for your team. Book it as a fixed-price gig, or hire me on Upwork for something bigger.