Hello everyone, I generated a webhook using GraphQL in my custom app for filtering purposes, but now I’m stuck on how to validate it in Laravel. Does anyone have any suggestions on how to achieve this?
I have not tried this but hope this blog helps.
Hi, thanks but this is for webhook created in dashboard, my webhook is created using graphql.
The authentication method for webhooks is the same regardless of how you create them.
I have in laravel:
public function testwebhook(Request $request){
function validateWebhook($data, $hmacHeader, $sharedSecret) {
$calculatedHmac = base64_encode(hash_hmac('sha256', $data, $sharedSecret, true));
return hash_equals($calculatedHmac, $hmacHeader);
}
// Example usage
$data = file_get_contents('php://input'); // Raw request body
$hmacHeader = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256']; // HMAC header from Shopify
$sharedSecret = env('SHOPIFY_SHARED_SECRET'); // Your app's shared secret
if (validateWebhook($data, $hmacHeader, $sharedSecret)) {
Log::info("Shopify webhook working");
} else {
Log::info("Webhook doesn;t work");
}
}
Webhook generated with graphql:
{
"data": {
"webhookSubscriptionCreate": {
"webhookSubscription": {
"id": "gid://shopify/WebhookSubscription/1831320519038",
"topic": "PRODUCTS_UPDATE",
"filter": null,
"format": "JSON",
"endpoint": {
"__typename": "WebhookHttpEndpoint",
"callbackUrl": "https://example.com"
}
},
"userErrors": []
}
},
"extensions": {
"cost": {
"requestedQueryCost": 10,
"actualQueryCost": 10,
"throttleStatus": {
"maximumAvailable": 2000,
"currentlyAvailable": 1990,
"restoreRate": 100
}
}
}
}
And still doesn’t work, any ideas?
Are you creating a public app or custom app?
Everything looks okay, so I’d checked your secret value is correct.
Thanks for answering, yeah its a custom app from settings>apps>develop custom app
Thanks
Just to confirm your using the API Secret key from you app, and ensured there’s no spaces or anything around it?
Have you got any middleware or anything modifying the request beforehand?
Api secret key i’m using, no space or anything. I’ve disabled csrf token for this route and, i got the response from:
if (validateWebhook($data, $hmacHeader, $sharedSecret)) {
Log::info("Shopify webhook working");
} else {
Log::info("Webhook doesn't work");
}
Allways i’m getting: Log::info("Webhook doesn't work");
I would suggest also to double check the secret. Otherwise, I wonder if it could be an encoding problem: the $data
read from the webhook request body should be handled as UTF-8: I’m not sure how it works in PHP, but the hash_hmac
could give a different result with the wrong encoding.
So from the dashboard store at Settings>apps> App development >MyApp> API credentials>API secret key
public function testwebhook(Request $request)
{
function validateWebhook($data, $hmacHeader, $sharedSecret) {
// Shopify folosește hashing binar, forțăm acest format în PHP
$calculatedHmac = base64_encode(hash_hmac('sha256', $data, $sharedSecret, true)); // <-- TRUE for binary format
// Debugging avansat
Log::info("Final normalized JSON for hashing: " . $data);
Log::info("Calculated HMAC (Laravel): " . $calculatedHmac);
Log::info("Received HMAC (Shopify): " . $hmacHeader);
return hash_equals($calculatedHmac, $hmacHeader);
}
// Citiți exact JSON-ul brut
$data = file_get_contents('php://input');
$hmacHeader = $request->header('X-Shopify-Hmac-Sha256');
$sharedSecret = env('SHOPIFY_SHARED_SECRET'); // API Secret Key
Log::info("Hex dump of raw JSON: " . bin2hex($data));
// Debugging
Log::info("Raw request body: " . $data);
Log::info("Length of raw body: " . strlen($data));
if (validateWebhook($data, $hmacHeader, $sharedSecret)) {
Log::info("✅ Shopify webhook validation successful.");
return response()->json(['message' => 'Verified'], 200);
} else {
Log::info("❌ Shopify webhook validation failed.");
return response()->json(['message' => 'Verification failed'], 403);
}
}
Logs:
[2025-02-28 11:47:25] production.INFO: Hex dump of raw JSON: 7b2261646d696e5f6772617068716c5f6170695f6964223a226769643a5c2f5c2f73686f706966795c2f50726f647563745c2f3134393639353230363835343338222c22626f64795f68746d6c223a225c7530303363705c7530303365315c75303033635c2f705c7530303365222c22637265617465645f6174223a22323032352d30322d32355431303a33373a30332d30353a3030222c2268616e646c65223a2274657374222c226964223a31343936393532303638353433382c2270726f647563745f74797065223a22222c227075626c69736865645f6174223a22323032352d30322d32355431303a33373a30332d30353a3030222c2274656d706c6174655f737566666978223a22222c227469746c65223a227465737432333332323233333535363637313131222c22757064617465645f6174223a22323032352d30322d32385430363a33393a32302d30353a3030222c2276656e646f72223a2265696c756d696e617473796e63222c22737461747573223a22616374697665222c227075626c69736865645f73636f7065223a22676c6f62616c222c2274616773223a22222c2276617269616e7473223a5b7b2261646d696e5f6772617068716c5f6170695f6964223a226769643a5c2f5c2f73686f706966795c2f50726f6475637456617269616e745c2f3535323435373235353635333130222c22626172636f6465223a22222c22636f6d706172655f61745f7072696365223a22322e3030222c22637265617465645f6174223a22323032352d30322d32355431303a33373a30342d30353a3030222c2266756c66696c6c6d656e745f73657276696365223a226d616e75616c222c226964223a35353234353732353536353331302c22696e76656e746f72795f6d616e6167656d656e74223a2273686f70696679222c22696e76656e746f72795f706f6c696379223a2264656e79222c22706f736974696f6e223a312c227072696365223a223434342e3030222c2270726f647563745f6964223a31343936393532303638353433382c22736b75223a22222c2274617861626c65223a747275652c227469746c65223a2244656661756c74205469746c65222c22757064617465645f6174223a22323032352d30322d32355431313a30313a30352d30353a3030222c226f7074696f6e31223a2244656661756c74205469746c65222c226f7074696f6e32223a6e756c6c2c226f7074696f6e33223a6e756c6c2c226772616d73223a302c22696d6167655f6964223a6e756c6c2c22776569676874223a302e302c227765696768745f756e6974223a226b67222c22696e76656e746f72795f6974656d5f6964223a35333638323338333738323237302c22696e76656e746f72795f7175616e74697479223a302c226f6c645f696e76656e746f72795f7175616e74697479223a302c2272657175697265735f7368697070696e67223a747275657d5d2c226f7074696f6e73223a5b7b226e616d65223a225469746c65222c226964223a31373331353433383932303036322c2270726f647563745f6964223a31343936393532303638353433382c22706f736974696f6e223a312c2276616c756573223a5b2244656661756c74205469746c65225d7d5d2c22696d61676573223a5b5d2c22696d616765223a6e756c6c2c2276617269616e745f67696473223a5b7b2261646d696e5f6772617068716c5f6170695f6964223a226769643a5c2f5c2f73686f706966795c2f50726f6475637456617269616e745c2f3535323435373235353635333130222c22757064617465645f6174223a22323032352d30322d32355431363a30313a30352e3030305a227d5d7d
[2025-02-28 11:47:25] production.INFO: Raw request body: {"admin_graphql_api_id":"gid:\/\/shopify\/Product\/14969520685438","body_html":"\u003cp\u003e1\u003c\/p\u003e","created_at":"2025-02-25T10:37:03-05:00","handle":"test","id":14969520685438,"product_type":"","published_at":"2025-02-25T10:37:03-05:00","template_suffix":"","title":"test2332223355667111","updated_at":"2025-02-28T06:39:20-05:00","vendor":"eiluminatsync","status":"active","published_scope":"global","tags":"","variants":[{"admin_graphql_api_id":"gid:\/\/shopify\/ProductVariant\/55245725565310","barcode":"","compare_at_price":"2.00","created_at":"2025-02-25T10:37:04-05:00","fulfillment_service":"manual","id":55245725565310,"inventory_management":"shopify","inventory_policy":"deny","position":1,"price":"444.00","product_id":14969520685438,"sku":"","taxable":true,"title":"Default Title","updated_at":"2025-02-25T11:01:05-05:00","option1":"Default Title","option2":null,"option3":null,"grams":0,"image_id":null,"weight":0.0,"weight_unit":"kg","inventory_item_id":53682383782270,"inventory_quantity":0,"old_inventory_quantity":0,"requires_shipping":true}],"options":[{"name":"Title","id":17315438920062,"product_id":14969520685438,"position":1,"values":["Default Title"]}],"images":[],"image":null,"variant_gids":[{"admin_graphql_api_id":"gid:\/\/shopify\/ProductVariant\/55245725565310","updated_at":"2025-02-25T16:01:05.000Z"}]}
[2025-02-28 11:47:25] production.INFO: Length of raw body: 1345
[2025-02-28 11:47:25] production.INFO: Final normalized JSON for hashing: {"admin_graphql_api_id":"gid:\/\/shopify\/Product\/14969520685438","body_html":"\u003cp\u003e1\u003c\/p\u003e","created_at":"2025-02-25T10:37:03-05:00","handle":"test","id":14969520685438,"product_type":"","published_at":"2025-02-25T10:37:03-05:00","template_suffix":"","title":"test2332223355667111","updated_at":"2025-02-28T06:39:20-05:00","vendor":"eiluminatsync","status":"active","published_scope":"global","tags":"","variants":[{"admin_graphql_api_id":"gid:\/\/shopify\/ProductVariant\/55245725565310","barcode":"","compare_at_price":"2.00","created_at":"2025-02-25T10:37:04-05:00","fulfillment_service":"manual","id":55245725565310,"inventory_management":"shopify","inventory_policy":"deny","position":1,"price":"444.00","product_id":14969520685438,"sku":"","taxable":true,"title":"Default Title","updated_at":"2025-02-25T11:01:05-05:00","option1":"Default Title","option2":null,"option3":null,"grams":0,"image_id":null,"weight":0.0,"weight_unit":"kg","inventory_item_id":53682383782270,"inventory_quantity":0,"old_inventory_quantity":0,"requires_shipping":true}],"options":[{"name":"Title","id":17315438920062,"product_id":14969520685438,"position":1,"values":["Default Title"]}],"images":[],"image":null,"variant_gids":[{"admin_graphql_api_id":"gid:\/\/shopify\/ProductVariant\/55245725565310","updated_at":"2025-02-25T16:01:05.000Z"}]}
[2025-02-28 11:47:25] production.INFO: Calculated HMAC (Laravel): ZbcE+LcDt6cLqhMyG2/qD3AMdeTv33ng9KzWDv2za+Q=
[2025-02-28 11:47:25] production.INFO: Received HMAC (Shopify): OQhxd10E010SktB2YcKti1z9Q4DcU3fErsBHfdOQbuA=
[2025-02-28 11:47:25] production.INFO: ❌ Shopify webhook validation failed.
I’ve installed Telescope from laravel just to see all requests.
I’d suggest making sure the data is encoded in UTF-8 when hashing it. I’m not a PHP expert but it looks like mb_convert_encoding
could help. Also I wonder if hash_equals
wouldn’t be better suited to compare the signature in the header and the computed hash.
I’ve tried and still don’t work, i’ve checked also byte per byte.
Controller:
public function testwebhook(Request $request)
{
function validateWebhook($data, $hmacHeader, $sharedSecret) {
// Force UTF-8 encoding to match Shopify's expectation
$utf8Data = mb_convert_encoding($data, 'UTF-8');
// Shopify uses raw binary format for HMAC hashing
$calculatedHmac = base64_encode(hash_hmac('sha256', $utf8Data, $sharedSecret, true));
// Debugging
Log::info("Final JSON for hashing (UTF-8 enforced): " . $utf8Data);
Log::info("Calculated HMAC: " . $calculatedHmac);
Log::info("Received HMAC: " . $hmacHeader);
return hash_equals($calculatedHmac, $hmacHeader);
}
// Get raw JSON body from Shopify
$data = file_get_contents('php://input');
$hmacHeader = $request->header('X-Shopify-Hmac-Sha256');
$sharedSecret = "xxxxxxxx"; // API Secret Key
// Debugging before hashing
Log::info("Raw request body: " . $data);
Log::info("Length of raw body: " . strlen($data));
if (validateWebhook($data, $hmacHeader, $sharedSecret)) {
Log::info("✅ Shopify webhook validation successful.");
return response()->json(['message' => 'Verified'], 200);
} else {
Log::info("❌ Shopify webhook validation failed.");
return response()->json(['message' => 'Verification failed'], 403);
}
}
Logs:
[2025-02-28 18:09:28] production.INFO: Raw request body: {"admin_graphql_api_id":"gid:\/\/shopify\/Product\/14969520685438","body_html":"\u003cp\u003e1d1d1f1\u003c\/p\u003e","created_at":"2025-02-25T10:37:03-05:00","handle":"test","id":14969520685438,"product_type":"","published_at":"2025-02-25T10:37:03-05:00","template_suffix":"","title":"aaaaaaadddd","updated_at":"2025-02-28T13:08:24-05:00","vendor":"eiluminatsync","status":"active","published_scope":"global","tags":"","variants":[{"admin_graphql_api_id":"gid:\/\/shopify\/ProductVariant\/55245725565310","barcode":"","compare_at_price":"2.00","created_at":"2025-02-25T10:37:04-05:00","fulfillment_service":"manual","id":55245725565310,"inventory_management":"shopify","inventory_policy":"deny","position":1,"price":"444.00","product_id":14969520685438,"sku":"","taxable":true,"title":"Default Title","updated_at":"2025-02-25T11:01:05-05:00","option1":"Default Title","option2":null,"option3":null,"grams":0,"image_id":null,"weight":0.0,"weight_unit":"kg","inventory_item_id":53682383782270,"inventory_quantity":0,"old_inventory_quantity":0,"requires_shipping":true}],"options":[{"name":"Title","id":17315438920062,"product_id":14969520685438,"position":1,"values":["Default Title"]}],"images":[],"image":null,"variant_gids":[{"admin_graphql_api_id":"gid:\/\/shopify\/ProductVariant\/55245725565310","updated_at":"2025-02-25T16:01:05.000Z"}]}
[2025-02-28 18:09:28] production.INFO: Length of raw body: 1342
[2025-02-28 18:09:28] production.INFO: Hex dump of JSON being hashed: 7b2261646d696e5f6772617068716c5f6170695f6964223a226769643a5c2f5c2f73686f706966795c2f50726f647563745c2f3134393639353230363835343338222c22626f64795f68746d6c223a225c7530303363705c7530303365316431643166315c75303033635c2f705c7530303365222c22637265617465645f6174223a22323032352d30322d32355431303a33373a30332d30353a3030222c2268616e646c65223a2274657374222c226964223a31343936393532303638353433382c2270726f647563745f74797065223a22222c227075626c69736865645f6174223a22323032352d30322d32355431303a33373a30332d30353a3030222c2274656d706c6174655f737566666978223a22222c227469746c65223a226161616161616164646464222c22757064617465645f6174223a22323032352d30322d32385431333a30383a32342d30353a3030222c2276656e646f72223a2265696c756d696e617473796e63222c22737461747573223a22616374697665222c227075626c69736865645f73636f7065223a22676c6f62616c222c2274616773223a22222c2276617269616e7473223a5b7b2261646d696e5f6772617068716c5f6170695f6964223a226769643a5c2f5c2f73686f706966795c2f50726f6475637456617269616e745c2f3535323435373235353635333130222c22626172636f6465223a22222c22636f6d706172655f61745f7072696365223a22322e3030222c22637265617465645f6174223a22323032352d30322d32355431303a33373a30342d30353a3030222c2266756c66696c6c6d656e745f73657276696365223a226d616e75616c222c226964223a35353234353732353536353331302c22696e76656e746f72795f6d616e6167656d656e74223a2273686f70696679222c22696e76656e746f72795f706f6c696379223a2264656e79222c22706f736974696f6e223a312c227072696365223a223434342e3030222c2270726f647563745f6964223a31343936393532303638353433382c22736b75223a22222c2274617861626c65223a747275652c227469746c65223a2244656661756c74205469746c65222c22757064617465645f6174223a22323032352d30322d32355431313a30313a30352d30353a3030222c226f7074696f6e31223a2244656661756c74205469746c65222c226f7074696f6e32223a6e756c6c2c226f7074696f6e33223a6e756c6c2c226772616d73223a302c22696d6167655f6964223a6e756c6c2c22776569676874223a302e302c227765696768745f756e6974223a226b67222c22696e76656e746f72795f6974656d5f6964223a35333638323338333738323237302c22696e76656e746f72795f7175616e74697479223a302c226f6c645f696e76656e746f72795f7175616e74697479223a302c2272657175697265735f7368697070696e67223a747275657d5d2c226f7074696f6e73223a5b7b226e616d65223a225469746c65222c226964223a31373331353433383932303036322c2270726f647563745f6964223a31343936393532303638353433382c22706f736974696f6e223a312c2276616c756573223a5b2244656661756c74205469746c65225d7d5d2c22696d61676573223a5b5d2c22696d616765223a6e756c6c2c2276617269616e745f67696473223a5b7b2261646d696e5f6772617068716c5f6170695f6964223a226769643a5c2f5c2f73686f706966795c2f50726f6475637456617269616e745c2f3535323435373235353635333130222c22757064617465645f6174223a22323032352d30322d32355431363a30313a30352e3030305a227d5d7d
[2025-02-28 18:09:28] production.INFO: Calculated HMAC: nkEmxSGOZOKKnv/yFGKJJplYQ26Ug9W8f301VaeOxF0=
[2025-02-28 18:09:28] production.INFO: Received HMAC: R/Ceqc9D8pP8+OUBNB7lF4EMPM9qLgFpjEsxkpzitV0=
[2025-02-28 18:09:28] production.INFO: ❌ Shopify webhook validation failed.
[2025-02-28 18:09:28] production.INFO: Byte-by-byte diff: False
I was curious about this, it can be because its a development store?
I tested with 2 different no code platforms with make.com webhook still doesn’t work, n8n the same thing.
Did you create the webhook using the same custom app, that the client secret is for?
I would take a step back and start from the beginning:
On your dev store, create a new custom app with the correct permissions.
Create the webhook using the Admin API token from your custom app.
Then the bottom secret from the custom app to verify the webhook.
Hi, thanks for fast response. The webhook was created from shopify app graphql in dashboard. This can be an issue?
Yeah, that’ll be it.
Just to clarify you mean using the Shopify iGraphQL App in Admin?
Your app can only authenticate webhooks created by itself.
So you’ll either need to create it using GraphQL with the Admin API Key of your custom app OR if you go into notifications in Shopify Admin settings, you can manually set these up and use the displayed token.
Yes, thats the app which i used to generate that webhook, thanks i will try to generate a webhook by a request graphql.
Great guys, thanks for patience. The issue it was i created the webhook from shopify dashboard Shopify iGraphQL App and that was the reason why wasn’t working to validation the of the webhook.
Thank you guys!