Join us for an upcoming Shopify Partner webinar on February 27, 2024. Discover the latest Checkout Extensibility features, and deep dive on improvements to Shopify Functions and Web Pixels. Register now for either the 10am EST or 2pm EST sessions.

HMAC validation fails when request body is empty

Shopify Partner
1 0 2


we are developing a fulfillment app and we have some problems with the HMAC validation.

We implemented the needed endpoints as described here:
and we validate the request as described here:

The validation suceeds as it should if there is data in the incoming request body but if the request body is empty (e.g. GET fetch_stock), the verification always fails.


Our validation function looks like this:


 * @param string $rawBody
 * @param string $hmac
 * @Return bool return true if callback could be verified
 * @throws UninitializedContextException
public static function validateCallbackHmac(string $rawBody, string $hmac): bool

    if (!hash_equals($hmac, base64_encode(hash_hmac('sha256', $rawBody, Context::$API_SECRET_KEY, true)))) {
        throw new InvalidCallbackException('Could not validate callback HMAC');
    return true;



Thank you in advance!

Replies 4 (4)

Shopify Partner
13 5 11

You need to use the query string instead of body.


example with fetch_stock.json request:


// max_retries=3&

Shopify Partner
3 0 0

Were you able to solve this? We are also facing the question how to verify requests to callback_url/fetch_stock, it's always failing at the moment. We are extracting the relevant query parameters, sort them, and calculate the digest - no luck so far



def verify(hmac, params)
   params = params.slice(:max_retries, :shop, :sku, :timestamp)
   digest = OpenSSL::HMAC.hexdigest("sha256"),
   ActiveSupport::SecurityUtils.secure_compare(hmac, digest)



Shopify Partner
13 5 11

hi hauraki


i'm not familar with ruby; in the example app from shopify i found the part with hmac validation, maybe it helps for you:



    def validate_hmac(hmac,request)
      h = request.params.reject{|k,_| k == 'hmac' || k == 'signature'}
      query = URI.escape(h.sort.collect{|k,v| "#{k}=#{v}"}.join('&'))
      digest = OpenSSL::HMAC.hexdigest('sha256'), API_SECRET, query)

      unless (hmac == digest)
        return [403, "Authentication failed. Digest provided was: #{digest}"]


Shopify Partner
3 0 0

Hi M8th, thanks for the code snippet. What we are doing is very similar, except that your example escapes the query string before digesting, while we unescape it. I tried escaping, but that also does not seem to solve the problem 😕