This is an article which is the updated version of my article Zoho Deluge: Push Item to Shopify API for 2022 using the new policy that Shopify have implemented when creating a custom app.
Why?
My use-case scenario here is that we have a Zoho Creator app which comprises of a custom quote builder that staff use to store details about a product, and then to push that product out to their Shopify store with a pre-built description, tags and inventory details. This means the app needs to be able to sync all of its products, customers, and orders with Shopify as a 2-way integration as the Zoho app needs information from additional apps installed on the Shopify store.
Why upgrade to the latest version for Shopify when private apps created before February 2022 won't be deprecated? Because the previous version was limited by our ability to search by product SKU. The new version, taking advantage of GraphQL, will be able to search by SKU and retrieve the correct product ID, variant ID, and inventory ID from Shopify.
How?
The below details on how we set up an access token in Shopify using the new process.
Preparation:
You don't have to do this, but I store all the keys in a Zoho Creator form (I call mine "API Integration") ready with the following fields:
- Connection Name (Single Line)
- Dev ID (Single Line)
- Client ID (Single Line)
- Shop ID (Single Line)
- Client Secret (Single Line)
- Location ID (Single Line)
- API Version (Single Line)
- Location Code (Origin) (Single Line)
- Authorize Endpoint (Single Line)
- Grant URL (Url)
- Token Endpoint (Single Line)
- Session ID (Single Line)
- Redirect URI (Single Line)
- Scope(s) (Multi Line)
- Access Token (Multi Line)
- Access Token Expiry (Date-Time)
- Refresh Token (Multi Line)
- Refresh Token Expiry (Date-Time)
- AuthToken (Multi Line)
- AuthToken Expiry (Date-Time)
* Some of these fields are unnecessary for this example but are used to accommodate other API integrations in the system that are using OAuth 2.0 or authtokens. Note how this is the same form in my article: Zoho Creator: Push to eBay Listings or my Zoho Deluge: Push Item to Shopify API.
Create an entry in our new form:
Access this application so you are viewing the front-end of your Creator app and go to your recently created API Integration form (tip: keep this open while you get the information from Shopify and your scripts found further below in this article):
- Give it a Connection Name (eg. "Shopify API Oauth")
- Enter the Shop ID (eg. "example-store.myshopify.com")
- Give it an API version (eg. "2022-01" - or whichever is the latest stable version)
- Location ID if you know it, otherwise leave blank (eg. "123456789")
- Location Code if you know it (eg. "GB")
Get access to the Shopify Admin Interface:
- Request access to the Shopify store owner as a developer
- Login to the Client's Shopify Admin as an invited developer
- Click on "Apps" > Develop Apps > Create an App
- Give it a name (eg. mycompanyname Zoho OAuth)
- Set its developer (preferably as the user you are logged in as)
- Click on "Create"
- First thing I do is browse to the API Credentials tab:
- Store the API Key in your creator form you created above in the "Client ID" field.
- Store the API Secret (click on the copy icon or the eye icon to copy & paste) into the "Client Secret" field.
- Click on "Configure Admin API scopes": This will be dependent on what your app needs to do. In this example, mine needs to sync orders, products, customers, and some reports; the other things like discounts, fulfillments will all be done by the sales team via Shopify. So I select the following: (see: Shopify API Access scopes for more info on each):
read_analytics,write_customers,read_customers,write_inventory,read_inventory,write_order_edits,read_order_edits,write_orders,read_orders,write_product_listings,read_product_listings,write_products,read_products,write_reports,read_reports
- Click on "Save"
- Click on "Install App" button in the top right
- Confirm by clicking on "Install" button in the popup
- You will be shown your Admin API access token > Store this in the Access Token field of your Creator form.
REST Admin API: Usage:
Here's a quick test I do retrieving 5 products using the REST admin API:
// // app specific r_ShopifyAPI = API_Integration[Connection_Name == "Shopify API OAuth"]; v_ShopID = r_ShopifyAPI.Shop_ID; v_ShopifyApiVersion = r_ShopifyAPI.API_Version; v_AccessToken = r_ShopifyAPI.Access_Token; // // set header parameters m_Header = Map(); m_Header.put("Content-Type","application/json"); m_Header.put("X-Shopify-Access-Token", v_AccessToken); // // set endpoint to retrieve 5 products v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/products.json?limit=5"; // // curl (zoho invoke) request r_GetProduct = invokeurl [ url :v_Endpoint type :GET headers:m_Header ]; // // output info r_GetProduct;
- //
- // app specific
- r_ShopifyAPI = API_Integration[Connection_Name == "Shopify API OAuth"];
- v_ShopID = r_ShopifyAPI.Shop_ID;
- v_ShopifyApiVersion = r_ShopifyAPI.API_Version;
- v_AccessToken = r_ShopifyAPI.Access_Token;
- //
- // set header parameters
- m_Header = Map();
- m_Header.put("Content-Type","application/json");
- m_Header.put("X-Shopify-Access-Token", v_AccessToken);
- //
- // set endpoint to retrieve 5 products
- v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/products.json?limit=5";
- //
- // curl (zoho invoke) request
- r_GetProduct = invokeUrl
- [
- url :v_Endpoint
- type :GET
- headers:m_Header
- ];
- //
- // output
- info r_GetProduct;
GraphQL: Usage example:
And here's a quick test to retrieve the 5 recent products using the GraphQL API:
// // app specific r_ShopifyAPI = API_Integration[Connection_Name == "Shopify API OAuth"]; v_ShopID = r_ShopifyAPI.Shop_ID; v_ShopifyApiVersion = r_ShopifyAPI.API_Version; v_AccessToken = r_ShopifyAPI.Access_Token; // // set header parameters m_Header = Map(); m_Header.put("Content-Type","application/json"); m_Header.put("X-Shopify-Access-Token", v_AccessToken); // // graphql v_GraphQl = "{ products(first: 5, reverse: true) { edges { node { id title handle } } } }"; m_GraphQl = Map(); m_GraphQl.put("query", v_GraphQl); // // Let's test with this GraphQL query v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/graphql.json"; r_GetProduct = invokeurl [ url :v_Endpoint type :POST parameters: m_GraphQl.toString() headers:m_Header ]; info r_GetProduct;
- //
- // app specific
- r_ShopifyAPI = API_Integration[Connection_Name == "Shopify API OAuth"];
- v_ShopID = r_ShopifyAPI.Shop_ID;
- v_ShopifyApiVersion = r_ShopifyAPI.API_Version;
- v_AccessToken = r_ShopifyAPI.Access_Token;
- //
- // set header parameters
- m_Header = Map();
- m_Header.put("Content-Type","application/json");
- m_Header.put("X-Shopify-Access-Token", v_AccessToken);
- //
- // graphql
- v_GraphQl = "{ products(first: 5, reverse: true) { edges { node { id title handle } } } }";
- m_GraphQl = Map();
- m_GraphQl.put("query", v_GraphQl);
- //
- // Let's test with this GraphQL query
- v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/graphql.json";
- r_GetProduct = invokeUrl
- [
- url :v_Endpoint
- type :POST
- parameters: m_GraphQl.toString()
- headers:m_Header
- ];
- info r_GetProduct;
Awesome!
Now for the primary objective of this task, which was to recover the relevant product based on any given Product SKU. Making the above into a function as well as returning the inventory item ID and the product ID:
map API.fn_SearchShopifyBySKU( string p_SKU ) { // // app specific r_ShopifyAPI = API_Integration[Connection_Name == "Shopify API OAuth"]; v_ShopID = r_ShopifyAPI.Shop_ID; v_ShopifyApiVersion = r_ShopifyAPI.API_Version; v_AccessToken = r_ShopifyAPI.Access_Token; // // send through m_Header = Map(); m_Header.put("Content-Type","application/json"); m_Header.put("X-Shopify-Access-Token", v_AccessToken); // // graphql v_SearchSKU = ifnull(p_SKU,""); v_GraphQl = "{ productVariants(first: 1, query: \"sku:'"+v_SearchSKU+"'\") { edges { node { id price sku title barcode inventoryItem { id } product { id } } } } }"; m_GraphQl = Map(); m_GraphQl.put("query", v_GraphQl); // // Send this GraphQL query v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/graphql.json"; r_GetProduct = invokeurl [ url :v_Endpoint type :POST parameters: m_GraphQl.toString() headers:m_Header ]; return r_GetProduct.toMap(); }
- map API.fn_SearchShopifyBySKU( string p_SKU )
- {
- //
- // app specific
- r_ShopifyAPI = API_Integration[Connection_Name == "Shopify API OAuth"];
- v_ShopID = r_ShopifyAPI.Shop_ID;
- v_ShopifyApiVersion = r_ShopifyAPI.API_Version;
- v_AccessToken = r_ShopifyAPI.Access_Token;
- //
- // send through
- m_Header = Map();
- m_Header.put("Content-Type","application/json");
- m_Header.put("X-Shopify-Access-Token", v_AccessToken);
- //
- // graphql
- v_SearchSKU = ifnull(p_SKU,"");
- v_GraphQl = "{ productVariants(first: 1, query: \"sku:'"+v_SearchSKU+"'\") { edges { node { id price sku title barcode inventoryItem { id } product { id } } } } }";
- m_GraphQl = Map();
- m_GraphQl.put("query", v_GraphQl);
- //
- // Send this GraphQL query
- v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/graphql.json";
- r_GetProduct = invokeUrl
- [
- url :v_Endpoint
- type :POST
- parameters: m_GraphQl.toString()
- headers:m_Header
- ];
- return r_GetProduct.toMap();
- }
{ "data": { "productVariants": { "edges": [ { "node": { "id": "gid://shopify/ProductVariant/123456789012345", "price": "450.95", "sku": "ABC123456", "title": "Default Title", "barcode": "0000012345", "inventoryItem": { "id": "gid://shopify/InventoryItem/23456789123456" }, "product": { "id": "gid://shopify/Product/345678901234" } } } ] } }, "extensions": { "cost": { "requestedQueryCost": 5, "actualQueryCost": 5, "throttleStatus": { "maximumAvailable": 1000, "currentlyAvailable": 995, "restoreRate": 50 } } } }
- {
- "data": {
- "productVariants": {
- "edges": [
- {
- "node": {
- "id": "gid://shopify/ProductVariant/123456789012345",
- "price": "450.95",
- "sku": "ABC123456",
- "title": "Default Title",
- "barcode": "0000012345",
- "inventoryItem": {
- "id": "gid://shopify/InventoryItem/23456789123456"
- },
- "product": {
- "id": "gid://shopify/Product/345678901234"
- }
- }
- }
- ]
- }
- },
- "extensions": {
- "cost": {
- "requestedQueryCost": 5,
- "actualQueryCost": 5,
- "throttleStatus": {
- "maximumAvailable": 1000,
- "currentlyAvailable": 995,
- "restoreRate": 50
- }
- }
- }
- }
Additional
- Note that if your search query string has spaces in it, the above without the small apostrophes (single-quotes) will return either a parsing error or just no data. In the line: copyrawChange from \"sku:" + v_SearchSKU + "\" to \"sku:'" + v_SearchSKU + "'\"
v_GraphQl = "{ productVariants(first: 1, query: \"sku:" + v_SearchSKU + "\") { edges { node { id price sku title barcode inventoryItem { id } product { id } } } } }"
- v_GraphQl = "{ productVariants(first: 1, query: \"sku:" + v_SearchSKU + "\") { edges { node { id price sku title barcode inventoryItem { id } product { id } } } } }"
So instead of: query: "sku:ABC 123" send query: "sku:'ABC 123'" - The final graphQL for product id, variant id, inventory id, quantity, price barcode and handle?copyraw
// // graphql v_SearchSKU = ifnull(v_SKU,""); v_GraphQl = "{ productVariants(first: 1, query: \"sku:'" + v_SearchSKU + "'\") { edges { node { id price inventoryQuantity sku barcode inventoryItem { id } product { id handle } } } } }"; m_GraphQl = Map(); m_GraphQl.put("query",v_GraphQl); // // Send this GraphQL query v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/graphql.json"; r_GetProduct = invokeurl [ url :v_Endpoint type :POST parameters:m_GraphQl.toString() headers:m_Header ]; // // parse the response if(!isnull(r_GetProduct.get("data"))) { if(!isnull(r_GetProduct.get("data").get("productVariants"))) { if(!isnull(r_GetProduct.get("data").get("productVariants").get("edges"))) { l_Edges = r_GetProduct.get("data").get("productVariants").get("edges"); for each r_Node in l_Edges { m_Node = r_Node.get("node"); v_ProductID = m_Node.get("product").get("id").getSuffix("Product/"); v_VariantID = m_Node.get("id").getSuffix("ProductVariant/"); v_InventoryID = m_Node.get("inventoryItem").get("id").getSuffix("InventoryItem/"); v_Handle = m_Node.get("product").get("handle"); v_Price = m_Node.get("price"); v_Qty = m_Node.get("inventoryQuantity"); v_Barcode = m_Node.get("barcode"); } } } }
- //
- // graphql
- v_SearchSKU = ifnull(v_SKU,"");
- v_GraphQl = "{ productVariants(first: 1, query: \"sku:'" + v_SearchSKU + "'\") { edges { node { id price inventoryQuantity sku barcode inventoryItem { id } product { id handle } } } } }";
- m_GraphQl = Map();
- m_GraphQl.put("query",v_GraphQl);
- //
- // Send this GraphQL query
- v_Endpoint = "https://" + v_ShopID + "/admin/api/" + v_ShopifyApiVersion.toString() + "/graphql.json";
- r_GetProduct = invokeUrl
- [
- url :v_Endpoint
- type :POST
- parameters:m_GraphQl.toString()
- headers:m_Header
- ];
- //
- // parse the response
- if(!isnull(r_GetProduct.get("data")))
- {
- if(!isnull(r_GetProduct.get("data").get("productVariants")))
- {
- if(!isnull(r_GetProduct.get("data").get("productVariants").get("edges")))
- {
- l_Edges = r_GetProduct.get("data").get("productVariants").get("edges");
- for each r_Node in l_Edges
- {
- m_Node = r_Node.get("node");
- v_ProductID = m_Node.get("product").get("id").getSuffix("Product/");
- v_VariantID = m_Node.get("id").getSuffix("ProductVariant/");
- v_InventoryID = m_Node.get("inventoryItem").get("id").getSuffix("InventoryItem/");
- v_Handle = m_Node.get("product").get("handle");
- v_Price = m_Node.get("price");
- v_Qty = m_Node.get("inventoryQuantity");
- v_Barcode = m_Node.get("barcode");
- }
- }
- }
- }
-
For anyone interested in the GraphQL to query using the inventoryLevel connection, try the following to get the Locations:
copyrawThen using the Location of your product variant (where "1234567890" is the location ID) to get the recent 10 products updated today:
v_GraphQl = "{locations(first: 5, reverse: true) { edges { node { id name } } } }";
- v_GraphQl = "{locations(first: 5, reverse: true) { edges { node { id name } } } }";
copyrawI found this is the same as the inventoryQuantity so might as well reduce those costs (by almost a half!).{ productVariants( first: 10 reverse: true query: "updated_at:>'2022-02-14T00:00:00+0000'" ) { edges { node { id price compareAtPrice sku barcode inventoryQuantity inventoryItem { id inventoryLevel(locationId: "gid://shopify/Location/1234567890") { id available updatedAt } } product { id title handle } updatedAt } } } }
- {
- productVariants(
- first: 10
- reverse: true
- query: "updated_at:>'2022-02-14T00:00:00+0000'"
- ) {
- edges {
- node {
- id
- price
- compareAtPrice
- sku
- barcode
- inventoryQuantity
- inventoryItem {
- id
- inventoryLevel(locationId: "gid://shopify/Location/1234567890") {
- id
- available
- updatedAt
- }
- }
- product {
- id
- title
- handle
- }
- updatedAt
- }
- }
- }
- }
Source(s):
- Shopify Partners - How to Generate a Shopify Access Token (legacy)
- Shopify Community - API To Search Products By SKU
- Zoho Deluge: Push Item to Shopify API
- Shopify .dev - Getting started with the GraphQL Admin and REST Admin APIs