Shopify gives merchants a rich dataset. The platform makes it easy to keep track of key metrics like monthly revenue, gross margins, etc. The problem is if your company uses Notion, all the data relating to the company is on Notion except sales data. That's stuck in Shopify.
This tutorial will show you how to free sales data from Shopify and send it to Notion automatically. Whenever a new order is created in Shopify, the Notion page is updated.
In this tutorial, we'll learn how to automate tasks in Shopify using Gadget. This is a super valuable skill. Companies pay big bucks (thousands and thousands of $$) for automatons like this one.
The integration works like this: when a new order is created in Shopify, a backend service (built with Gadget) calculates the relevant financial metrics and pushes them as new entries into a Notion database.
By the end, you'll have a fault-tolerant system that handles rate limiting and ensures data accuracy. Instead of setting up all the rate limiting stuff ourselves, we can let Gadget handle it, and just write the business logic.
Prerequisites
- Access to the Notion finance dashboard template. This is where the sales data will be rendered. You can create your own Notion page to display the sales data, but I recommend you follow along with this page, then edit the page once everything is working. Just my 2 cents.
- A Shopify store with products set up (including the cost for each product in your store).
- You can set the cost of a product by going to Products > Select Product > Inventory > Cost per item. By default, this is set to $0, so update them manually.
- A Notion workspace.
- A free Gadget account
Step 1: Set Up the Notion Finance Dashboard
- Open the finance dashboard template.
- Click Duplicate in the top-right corner to copy it to your Notion workspace.
- Select your target workspace (personal or team) and confirm.
- Scroll to the bottom—the charts are powered by a single database. We'll write new rows to this database from Gadget. You can delete the dummy data I have in this database.
Step 2: Create a Gadget App and Connect to Shopify
Gadget acts as the "bridge" over the "river" between Shopify and Notion. It listens for new orders via webhooks and forwards calculated data to Notion.
- Go to gadget.dev and create a new account if needed.
- Click Create New App > Select Shopify app > Leave as Custom app > Continue.
- Keep defaults for frontend framework (Remix) and language (TypeScript).
- Name your app (e.g.,
finance-dashboard
) and confirm. - Go to Settings (bottom-left) > Plugins > Shopify.
- Go through the steps to connect your Gadget app to Shopify.
- Select these Shopify data models:
- Orders: For new order triggers.
-
Order Line Items: For prices and quantities.
- Products: Hidden in search—needed for variants.
-
Product Variants: For linking to inventory.
-
Inventory Items: For costs.
Use the search box to find these. Only enable Read permissions (no writes needed).
- Confirm the setup. Gadget auto-creates webhooks and data models.
- Fill out Shopify's protected customer data access form: Select App Functionality as the reason (to read prices/costs for Notion syncing) and save.
- Once the form is filled out, go back to Gadget. Install the app on your Shopify store when prompted.
- In Gadget, go to Settings > Plugins > Shopify—it should show as active.
- On the left side bar go to Installs > Sync data. This pulls your Shopify data into Gadget's database for redundancy and rate-limit protection.
- Verify in
api/models/shopifyOrder/data
: Check Shopify Orders for your orders.
Gadget's syncing ensures data consistency—if Shopify rate-limits requests during a sales surge, Gadget retries later, keeping Notion in sync.
Step 3: Configure the Notion Integration
We will create a custom internal integration in Notion to give Gadget access to the finance dashboard.
- Go to notion.so/profile/integrations
- Click New Integration.
- Name it (e.g., "Finance Dashboard Integration").
- Select your workspace (where the finance dashboard lives).
- Save and go to Access > Grant the integration access to the finance dashboard Notion page: Search for it, select, and update.
- Back in the Configuration tab, copy the Internal Integration Secret (API key).
- In Gadget, go to Settings > Environment Variables.
- Add:
- Key:
NOTION_API_KEY
| Value: Paste the secret (lock it for security if needed).
- Key:
- Get the Notion database ID:
- Scroll to the bottom of the finance database. Open the finance dashboard database as a full page.
- Copy the ID from the URL (between the last
/
and?
).- Add another variable: Key:
NOTION_DB_ID
| Value: Paste the ID.
- Add another variable: Key:
Step 4: Install Dependencies and Write the Code
Gadget auto-generates boilerplate code. We'll extend it to calculate metrics and update Notion.
- In Gadget's top search bar, run:
yarn add @notionhq/client
to install the Notion client library. - Go to Files > api > models > shopifyOrder > actions > create.ts
- Optional: Use a local editor instead of the browser-based one:
- Go to the cloud icon and follow the instructions:
- Now you can use your favorite editor to edit your Gadget app's code. Changes sync back to Gadget in real-time.
Replace the code in api/models/shopifyOrder/actions/create.ts
with the following:
import { applyParams, save, ActionOptions } from "gadget-server";
import { preventCrossShopDataAccess } from "gadget-server/shopify";
import { Client } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const updateNotionDb = async (
logger: any,
orderId: any,
type: any,
value: any
) => {
const response = await notion.pages.create({
parent: {
type: "database_id",
database_id: process.env.NOTION_DB_ID ?? "",
},
properties: {
"Order ID": {
type: "title",
title: [
{
type: "text",
text: {
content: orderId,
},
},
],
},
Type: {
type: "select",
select: {
name: type,
},
},
Value: {
type: "number",
number: value,
},
"Created At": {
type: "date",
date: {
start: new Date(Date.now()).toISOString(),
},
},
},
});
logger.debug("************ Notion response ************");
logger.debug(response);
};
export const run = async ({ params, record, logger, api, connections }) => {
applyParams(params, record);
await preventCrossShopDataAccess(params, record);
await save(record);
};
export const onSuccess = async ({
params,
record,
logger,
api,
connections,
}) => {
// Write new order to Notion
try {
logger.debug(`Calculating sales and COGS for order ${record.id}`);
const lineItems = await api.shopifyOrderLineItem.findMany({
filter: { orderId: { equals: record.id } },
select: {
id: true,
price: true,
quantity: true,
name: true,
variant: {
id: true,
title: true,
inventoryItem: {
id: true,
cost: true,
},
},
},
});
logger.debug(`Found ${lineItems.length} line items for order ${record.id}`);
let totalSales = 0;
let totalCOGS = 0;
let itemsWithoutInventory = 0;
for (const lineItem of lineItems) {
try {
const itemPrice = parseFloat(lineItem.price || "0");
const itemQuantity = lineItem.quantity || 0;
const itemSales = itemPrice * itemQuantity;
totalSales += itemSales;
if (lineItem.variant?.inventoryItem?.cost) {
const itemCost = parseFloat(lineItem.variant.inventoryItem.cost);
const itemCOGS = itemCost * itemQuantity;
totalCOGS += itemCOGS;
} else if (lineItem.variant) {
logger.debug(`Can't find inventory item for ${lineItem.variant}`);
} else {
logger.debug("Can't find variant");
}
logger.info(
`Order ${record.id} totals - Sales: $${totalSales} COGS: $${totalCOGS}`
);
if (totalCOGS > 0) {
await updateNotionDb(logger, record.id, "Sales", totalSales);
await updateNotionDb(logger, record.id, "Cost of Goods", totalCOGS);
await updateNotionDb(
logger,
record.id,
"Margin",
totalSales - totalCOGS
);
}
} catch (error) {
logger.error(error);
}
}
} catch (error) {
logger.error(error);
}
};
export const options: ActionOptions = { actionType: "create" };
Code Explanation
- Imports and Notion Client: Sets up Gadget utilities and initializes the Notion client with your API key.
-
updateNotionDb Function: Creates a new row in the Notion database. It populates columns: "Order ID" (title), "Type" (select: Sales, Cost of Goods, or Margin), "Value" (number), and "Created At" (date via ISO string).
- Why three rows per order? Notion requires this structure for grouping multi-line charts by a shared ID (Order ID). In other words: Notion's charting sucks. If you're interested, checkout this excellent tutorial on Notion charting.
- run Function: Gadget's entry point—applies params, prevents cross-shop access, and saves the order record.
-
onSuccess Function: Triggers after saving the order to the Gadget database:
- Fetches line items for the new order using Gadget's API (GraphQL-like query).
- Loops through items: Calculates sales (price × quantity) and COGS (cost × quantity).
- If COGS > 0, calls
updateNotionDb
three times (once per metric). - Error handling: Catches and logs issues to prevent crashes.
Step 5: Test the Integration
- In Shopify, go to Orders > Create Order.
- Add products, mark as paid, and create.
- Check Gadget's Logs (filter by error/debug) for issues.
- Refresh your Notion dashboard—new bars should appear for the order's sales, COGS, and margin.
- Verify in Notion's database: Three new rows per order, grouped by Order ID.
If nothing updates, check logs: Validation errors often stem from mismatched column names (case-sensitive) or missing costs in Shopify.
Troubleshooting
- No Data in Notion: Confirm environment variables, database ID, and integration access. Resync in Gadget.
- Rate Limiting: Gadget handles this—data will sync eventually.
- Errors in Logs: Debug with the built-in logger (e.g., missing variants mean no COGS calculated).
- Graph Issues: Ensure Notion column names match exactly (e.g., "Value" with capital V).
- Questions? Join the Gadget Developer Discord
Conclusion
Good on you for sticking to this tutorial to the end! Not a lot of people have the focus that you have.
If you like building custom software, and want to make bank doing it for big companies, checkout my other projects. Feel free to copy these projects and make money off them. Life isn't a zero sum game ❤️
Top comments (0)