Shopify - Support customer specific pricing

Set up a pricing endpoint/API

Create an API that:

  • uses a current customers location id and multiple productvariant id's as input.
  • fetches the current customer's prices from the Shopify GraphQL Admin API, using the input
  • returns the variantId and desired price as output.

The output would be in this format:

[
    {
        "id": "49328019669329",
        "price": "712.0"
    },
    {
        "id": "49328019997009",
        "price": "729.95"
    },
    {
        "id": "49328020324689",
        "price": "749.95"
    },
    {
        "id": "49328019538257",
        "price": "712.0"
    }
]

Example API

An example API in node.js that retrieves customer specific product prices from Shopify:

import express from "express";
import cors from "cors";
import ShopifyService from "./service";

const app = express();
const port = 3000;

app.use(cors());
app.use(express.json());

app.post("/api/pricing", async (req, res, next) => {
  const { locationId, variantIds } = req.body;

  var service = new ShopifyService();
  var data = await service.getForLocation(locationId, [], variantIds);

  var mapped = data.nodes?.map((n: any) => {
    return {
      id: n.id.replace("gid://shopify/ProductVariant/", ""),
      price: n.contextualPricing.price.amount,
    };
  });

  res.send(mapped);
});

app.listen(port, () => {
  console.log(`listening on port ${port}`);
});
import axios from "axios";

class ShopifyService {

  // Hardcoded credentials (matching your C# example)
  // Note: In a production app, use process.env.SHOPIFY_CLIENT_ID, etc.

  clientId = "";
  secret = "";
  shopUrl = "yourshop.myshopify.com";
  token = "";

  // GraphQL Queries
  private VARIANT_QUERY = `
        query getB2BVariantPrices($variantIds: [ID!]!, $locationId: ID!) {
            nodes(ids: $variantIds) {
                ... on ProductVariant {
                    id
                    variantId: id
                    contextualPricing(context: { companyLocationId: $locationId }) {
                      price {
                            amount
                            currencyCode
                        }
                    }
                }
            }
        }
    `;

  /**
   * Executes the GraphQL query against Shopify
   * @param {Object} queryPayload - The { query, variables } object
   */
  private async executeQuery(queryPayload: any) {
    // Check if token is missing, if so, fetch it
    if (!this.token || this.token.trim() === "") {
      try {
        this.token = await this.getOauthToken(
          this.shopUrl,
          this.clientId,
          this.secret
        );
        console.log("Token obtained: " + this.token);
      } catch (err: any) {
        console.error("Failed to obtain token:", err.message);
        return null;
      }
    }

    const url = `https://${this.shopUrl}/admin/api/2024-04/graphql.json`;

    try {
      const response = await axios.post(url, queryPayload, {
        headers: {
          "Content-Type": "application/json",
          "X-Shopify-Access-Token": this.token,
        },
      });

      // Axios throws on 4xx/5xx by default, but if Shopify returns 200 with errors in body:
      if (response.data.errors) {
        console.error("\x1b[31m%s\x1b[0m", "GraphQL Errors:"); // Red color
        console.error(JSON.stringify(response.data.errors, null, 2));
      }

      return response.data;
    } catch (error: any) {
      console.error("\x1b[31m%s\x1b[0m", "HTTP Request Error:"); // Red color
      if (error.response) {
        // Server responded with a status other than 2xx
        console.error(`Status: ${error.response.status}`);
        console.error(JSON.stringify(error.response.data, null, 2));
      } else {
        // Network error or setup error
        console.error(`Exception: ${error.message}`);
      }
      return null;
    }
  }

  /**
   * Main method to fetch prices
   * @param {string} locationId - The raw ID (e.g., "123")
   * @param {string[]} productIds - Array of raw product IDs
   * @param {string[]} variantIds - Array of raw variant IDs
   */
  public async getForLocation(
    locationId: string,
    productIds: string[],
    variantIds: string[]
  ) {
    // 1. Format IDs to Shopify GIDs
    const formattedLocationId = `gid://shopify/CompanyLocation/${locationId}`;

    // Note: In your C# code, you called .Select() but didn't assign the result back.
    // Here we map properly to ensure the GIDs are sent to the API.
    const formattedVariantIds = variantIds.map(
      (id) => `gid://shopify/ProductVariant/${id}`
    );

    // 2. Construct Payload
    const queryPayload = {
      query: this.VARIANT_QUERY,
      variables: {
        locationId: formattedLocationId,
        variantIds: formattedVariantIds,
      },
    };

    try {
      // Log the attempt (using JSON.stringify to mimic C# serialization logging)
      console.log(
        "\nFetching prices:",
        JSON.stringify({
          ids: formattedVariantIds,
          locationId: formattedLocationId,
        })
      );

      // 3. Execute
      const result = await this.executeQuery(queryPayload);
      return result?.data;
    } catch (ex: any) {
      console.error("\x1b[31m%s\x1b[0m", `Exception: ${ex.message}`);
    }
  }

  /**
   * Retrieves the OAuth token via Client Credentials flow
   */
  private async getOauthToken(
    shopifyDomain: string,
    shopifyClientId: string,
    shopifyClientSecret: string
  ) {
    const tokenEndpoint = `https://${shopifyDomain}/admin/oauth/access_token`;

    // URLSearchParams is the Node equivalent of FormUrlEncodedContent
    const params = new URLSearchParams();
    params.append("grant_type", "client_credentials");
    params.append("client_id", shopifyClientId);
    params.append("client_secret", shopifyClientSecret);

    try {
      const response = await axios.post(tokenEndpoint, params, {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      });

      const accessToken = response.data.access_token;

      if (!accessToken) {
        throw new Error(
          "Shopify OAuth response did not contain an access_token."
        );
      }

      return accessToken;
    } catch (error: any) {
      let errorMessage = "Shopify auth token request failed.";
      if (error.response) {
        errorMessage += ` (${error.response.status} ${error.response.statusText})`;
      }
      throw new Error(errorMessage);
    }
  }
}

export default ShopifyService;

Fetch the prices

In your theme, add a function that calls the API to fetch the prices and updates the Tweakwise JS tiles to display the correct price:

<script>
function tw__applyPricing(items){
  
  const productids = items.map(o => o.itemno);
  var locationId = {{ customer.current_location.id }};

  var body = {
    locationId: '{{customer.current_location.id}}',
    variantIds: productids
  };

  console.log('tw | getting pricing for', body)

  fetch('https://your.shop.com/api/pricing', {
    method: 'POST', 
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body) 
  })
    .then(response => response.json())
    .then(function(result){
    result.forEach(function(item){
      var element = document.querySelector('[data-item-id="' + item.id + '"] [data-property="price"]');
      if(element){
        element.innerHTML = item.price;
      }
    });

  });
  
}
</script>

Display the prices

Hook the function into Tweakwise JS to make sure the prices get displayed:

<script>
tweakwiseListerPage({
  //...
  on: {
      'twn.request.success': function (event) {
          tw__applyPricing(event.data);
        }
    }
})

tweakwiseRecommendations({
  //...
  on: {
      'twn.request.success': function (event) {
          tw__applyPricing(event.data.items);
        }
    }
})
</script>