Api call from Customer Account UI extension to app backend

I am working on an app where I need to make a fetch request from my customer account UI extension to my embedded app backend. I have tried with using app proxy or directly from the app url. All these approach gives me cors error. How can i make this api call succesfully?

1 Like

Did you configure CORS on the app backend?

Hi @Md_Tasin_Tahmid,

In order to make a Fetch request from a Customer Account UI Extension, you do need to have network_access set to true in your shopify.extension.toml configuration file, as well as requesting the use of network_access via your Partner Dashboard.

Additionally configuring CORS as @remy727 mentioned, you do need to ensure your app back end does respond to the HTTP requests with Access-Control-Allow-Origin: * in the Response Headers

And finally there is also some additional limitations and considerations that you should be aware of if you are trying to make the HTTP requests to an App Proxy URL specifically:

Thanks @remy727 and @Kellan-Shopify for your support.

I could not make it using app proxy.

This is how I am sending the fetch request from customer UI:

const response = await fetch('https://bio-grown-sacred-owned.trycloudflare.com/api/discount', {

        method: 'POST',

        headers: {

          'Content-Type': 'application/json',

          Authorization: `Bearer ${sessionToken}`,

        },

        body: JSON.stringify(payload),

      });

And in the backend this is how I am recieving the request:

export const action = async ({ request }) => {

  const origin = request.headers.get("Origin");

  

  if (origin !== "https://extensions.shopifycdn.com") {

    return new Response("Forbidden", { status: 403 });

  }

  

  const auth = await authenticate.public.customerAccount(request);

  const payload = await request.json();

  const { totalPoints, cost, discount, discountType, customerId } = payload;




  const {admin} = await shopify.unauthenticated.admin("store-themeog.myshopify.com");




  const customerGetsQuery = discountType == "Percentage" ? 

                  `... on DiscountPercentage {

                    percentage

                  }` :

                  `... on DiscountAmount {

                    amount {

                      amount

                    }

                    appliesOnEachItem

                  }`;

  const CREATE_DISCOUNT_CODE_MUTATION = `

    mutation CreateDiscountCode($basicCodeDiscount: DiscountCodeBasicInput!) {

      discountCodeBasicCreate(basicCodeDiscount: $basicCodeDiscount) {

        codeDiscountNode {

          id

          codeDiscount {

            ... on DiscountCodeBasic {

              title

              startsAt

              customerGets {

                value {

                  ${customerGetsQuery}

                }

              }

            }

          }

        }

        userErrors {

          field

          message

        }

      }

    }

  `;




  const discoutCode = "point_exchange"+random10DigitNumber();

  const customerGetsValue = discountType == "Percentage" ? { percentage: discount/100} : { "discountAmount": {"amount":discount*1.000}};




  const CREATE_DISCOUNT_CODE_MUTATION_VARIABLES = {

    basicCodeDiscount: {

      title: discoutCode,

      code: discoutCode,

      startsAt: "2025-12-04T00:00:00Z",

      customerSelection: {

        customers: {

          add: [`${customerId}`],

        },

      },

      customerGets: {

        value: customerGetsValue,

        items: {

          all: true,

        },

      },

      minimumRequirement: {

        subtotal: {

          greaterThanOrEqualToSubtotal: "50.0",

        },

      },

      usageLimit: 1,

      appliesOncePerCustomer: true,

    },

  };




  let discountResponse;




  try {

    const response = await admin.graphql(CREATE_DISCOUNT_CODE_MUTATION, {

      variables: CREATE_DISCOUNT_CODE_MUTATION_VARIABLES,

    });




    const discountData = await response.json();




    discountResponse = discountData;

  } catch (error) {

    return new Response(

      JSON.stringify({ success: false, error: error }),

      {

        status: 500,

          headers: {

          "Access-Control-Allow-Origin": origin,

          "Access-Control-Allow-Headers": "Authorization, Content-Type",

          "Access-Control-Allow-Methods": "GET, POST, OPTIONS",

        },

      },

    );

  }




  const updateCustomerMetafieldsMutation = `mutation updateCustomerMetafield($input: CustomerInput!, $namespace: String!, $key: String!) {

    customerUpdate(input: $input) {

      customer {

        id

        metafield(namespace: $namespace, key: $key) {

          namespace

          key

          type

          value

        }

      }

      userErrors {

        field

        message

      }

    }

  }`;





  const updateCustomerMetafieldsVariables = {

    "input": {

      "id": `${customerId}`,

      "metafields": [

        {

          "namespace": "custom",

          "key": "total_points_points_exchange",

          "type": "number_integer",

          "value": `${totalPoints-cost}`

        }

      ]

    },

    "namespace": "custom",

    "key": "total_points_points_exchange"

  };




  let customerMetafieldsResponse;




  try {

    const response = await admin.graphql(updateCustomerMetafieldsMutation, {

      variables: updateCustomerMetafieldsVariables,

    });




    const { data } = await response.json();




    console.log("Updated Metafield:::", data.customerUpdate.customer)

    customerMetafieldsResponse =  data.customerUpdate.customer.metafield;

  } catch (error) {

    return new Response(

      JSON.stringify({ success: false, error: error }),

      {

        status: 500,

          headers: {

          "Access-Control-Allow-Origin": origin,

          "Access-Control-Allow-Headers": "Authorization, Content-Type",

          "Access-Control-Allow-Methods": "GET, POST, OPTIONS",

        },

      },

    );

  }




  if(discountResponse && customerMetafieldsResponse){

    const finalResponse = {discountResponse, customerMetafieldsResponse}

    return new Response(JSON.stringify({ success: true, finalResponse }), {

      status: 201,

      headers: {

        "Access-Control-Allow-Origin": origin,

        "Access-Control-Allow-Headers": "Authorization, Content-Type",

        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",

      },

    });

  }




};

This is working though. But is it a good solution?

Hi @Md_Tasin_Tahmid,

Thank you for sharing that context. After a quick review of the code you shared I have a couple points to suggest.

  1. this may be used elsewhere, but the auth constant you are defining and setting with await authenticate.public.customerAccount(request) is not in use anywhere in the code you shared

  2. In the Response Headers for the Request from the Customer Account Extension should have the Access-Control-Allow-Origin header set to the wildcard character * , instead you’re setting it to https://extensions.shopifycdn.com

I believe both of these points above can be corrected with the documentation as follows:

The authenticate.public.customerAccount function ensures that customer account extension requests are coming from Shopify, and returns helpers to respond with the correct headers.

With the Shopify App Remix package, you should use the cors value returned from the authenticate.public.customerAccount() method to set the correct response headers for the actual Customer Account Extension Request.

This makes it so you don’t have to specify the origin or even pass Access-Control-Allow-Origin: * manually, as it will set it for you if you use the cors value when creating the actual HTTP Response Headers.

Additionally this method returns the session token from the request and can be used to authenticate further API calls, instead of using the unauthenticated authorization method you are currently using.

  • const {admin} = await shopify.unauthenticated.admin("store-themeog.myshopify.com");

Regarding App Proxy Requests, they should work with Customer Account Extensions, as mentioned in the Shopify.dev documentation, though there are some limitations and requirements needed to ensure it works correctly. If you’re wanting to use App Proxies specifically, please review the following and if you’re sure you’re complying with these requirements correctly we can help look into it further with specific examples where it’s not working as expected. If that is the case I would recommend recording a HAR file showing the proxy requests not behaving as expected, and reach out to our Shopify Support Team via the Shopify Help Center and we can help look into it further.

UI extensions can make fetch requests to App Proxy URLs, but there are some differences and limitations related to the security context within which UI extensions run.

UI extension requests made to the App Proxy will execute as CORS requests. See Required CORS headers above for information about requirements related to CORS.

UI extension requests made to the App Proxy will not assign the logged_in_customer_id query parameter. Instead use a session token which provides the sub claim for the logged in customer.

UI extension requests made to the App Proxy of password protected shops is not supported. Extension requests come from a web worker which does not share the same session as the parent window.

The App Proxy doesn’t handle all HTTP request methods. Specifically, CONNECT and TRACE are unsupported.

Hi @Md_Tasin_Tahmid,

I just wanted to follow up here and see if this helped answer your questions above?

If so we can go ahead and mark this thread as solved, otherwise I’m happy to help with any further questions you have!