Prerequisites
- A Shopify store with webhook access (Shopify admin, REST API, or GraphQL Admin API)
- A WhatsApp Business Account connected through Dualhook
- Webhook Override configured for direct message routing
- A server to receive both Shopify and WhatsApp webhooks (Node.js, Laravel, or any HTTP server)
- Approved WhatsApp message templates for your order notifications
- Customer opt-in for WhatsApp notifications (required by WhatsApp template policy)
Set these environment variables:
SHOPIFY_WEBHOOK_SECRET=your_shopify_secret
WHATSAPP_ACCESS_TOKEN=your_access_token
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_WABA_ID=your_waba_id
META_GRAPH_VERSION=v25.0
No
META_APP_SECRET? In the Embedded Signup flowX-Hub-Signature-256is signed by Dualhook's Meta app and isn't customer-verifiable — validate inbound WhatsApp webhooks by payload shape instead.SHOPIFY_WEBHOOK_SECRETis unaffected. See Messaging Webhook → Inbound Webhook POST Validation.
Architecture Overview
This integration connects two webhook flows through your server:
- Shopify order events — Shopify sends webhooks to your server when orders are created, fulfilled, or cancelled.
- WhatsApp template messages — Your server sends template messages to customers via the Cloud API when order events occur.
- WhatsApp replies — Meta delivers customer replies directly to your server via Webhook Override. Dualhook is not used as a message-storage layer.
Shopify → Your Server → Meta Cloud API → Customer WhatsApp
↑
Customer WhatsApp → Meta → Your Server (via Webhook Override)
Dualhook handles the initial WhatsApp configuration (Embedded Signup, Webhook Override, token management) and operational monitoring. Your server handles the business logic of connecting Shopify events to WhatsApp messages.
Receive Shopify Order Webhooks
Register Shopify webhooks via the Shopify Admin REST API or GraphQL Admin API for the order events you want to trigger WhatsApp messages. Shopify requires your server to respond within 5 seconds or the delivery is considered failed.
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
const SHOPIFY_SECRET = process.env.SHOPIFY_WEBHOOK_SECRET;
// Verify Shopify webhook signature (HMAC-SHA256, base64-encoded)
function verifyShopify(req) {
const hmac = req.headers["x-shopify-hmac-sha256"];
if (!hmac) return false;
const hash = crypto
.createHmac("sha256", SHOPIFY_SECRET)
.update(req.rawBody)
.digest("base64");
const hmacBuf = Buffer.from(hmac);
const hashBuf = Buffer.from(hash);
// Guard against length mismatch — timingSafeEqual throws if lengths differ
if (hmacBuf.length !== hashBuf.length) return false;
return crypto.timingSafeEqual(hmacBuf, hashBuf);
}
// Normalise phone to E.164 format (WhatsApp requires this)
function toE164(phone) {
if (!phone) return null;
const digits = phone.replace(/\D/g, "");
if (!digits) return null;
return digits.startsWith("+") ? digits : "+" + digits;
}
app.post("/shopify/orders/create", (req, res) => {
if (!verifyShopify(req)) return res.sendStatus(401);
res.sendStatus(200);
const order = req.body;
const phone = toE164(order.customer?.phone || order.shipping_address?.phone);
if (phone) {
sendOrderConfirmation(phone, order);
}
});
app.post("/shopify/orders/fulfilled", (req, res) => {
if (!verifyShopify(req)) return res.sendStatus(401);
res.sendStatus(200);
const order = req.body;
const phone = toE164(order.customer?.phone || order.shipping_address?.phone);
if (phone) {
sendFulfillmentNotification(phone, order);
}
});
Send WhatsApp Templates on Order Events
When a Shopify order event fires, send the corresponding WhatsApp template via the Cloud API. Your server talks to Meta directly — Dualhook configures the webhook routing but is not in the sending path.
The to field must be in E.164 format (e.g. +14155552671). Only send template messages to customers who have opted in to receive WhatsApp notifications.
const PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID;
const ACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN;
const API_VERSION = process.env.META_GRAPH_VERSION || "v25.0";
async function sendOrderConfirmation(customerPhone, order) {
const response = await fetch(
`https://graph.facebook.com/${API_VERSION}/${PHONE_NUMBER_ID}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
messaging_product: "whatsapp",
to: customerPhone,
type: "template",
template: {
name: "order_confirmation",
language: { code: "en" },
components: [
{
type: "body",
parameters: [
{ type: "text", text: order.name }, // Order number
{ type: "text", text: order.total_price }, // Total
{ type: "text", text: order.currency }, // Currency
],
},
],
},
}),
}
);
if (!response.ok) {
const error = await response.json();
console.error("WhatsApp send failed:", error);
}
}
async function sendFulfillmentNotification(customerPhone, order) {
const trackingUrl =
order.fulfillments?.[0]?.tracking_urls?.[0] ??
order.fulfillments?.[0]?.tracking_url ??
"No tracking available";
await fetch(
`https://graph.facebook.com/${API_VERSION}/${PHONE_NUMBER_ID}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
messaging_product: "whatsapp",
to: customerPhone,
type: "template",
template: {
name: "order_shipped",
language: { code: "en" },
components: [
{
type: "body",
parameters: [
{ type: "text", text: order.name },
{ type: "text", text: trackingUrl },
],
},
],
},
}),
}
);
}
Receive WhatsApp Replies
When customers reply to your template messages, Meta delivers the reply directly to your server via Webhook Override. Handle these replies on the same server that receives Shopify webhooks. Meta expects a 200 response within ~10 seconds. Validate by envelope, WABA ID, and phone_number_id.
const EXPECTED_WABA_ID = process.env.WHATSAPP_WABA_ID;
const EXPECTED_PHONE_NUMBER_ID = process.env.WHATSAPP_PHONE_NUMBER_ID;
function isExpectedWhatsAppPayload(body) {
if (!body || body.object !== "whatsapp_business_account") return false;
if (!Array.isArray(body.entry) || body.entry.length === 0) return false;
for (const entry of body.entry) {
if (entry.id !== EXPECTED_WABA_ID) return false;
if (!Array.isArray(entry.changes) || entry.changes.length === 0) return false;
for (const change of entry.changes) {
if (change.field !== "messages") return false;
const phoneNumberId = change.value?.metadata?.phone_number_id;
if (phoneNumberId !== EXPECTED_PHONE_NUMBER_ID) return false;
}
}
return true;
}
app.post("/whatsapp/webhook", (req, res) => {
if (!isExpectedWhatsAppPayload(req.body)) return res.sendStatus(401);
res.sendStatus(200);
for (const entry of req.body.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.field !== "messages") continue;
for (const message of change.value.messages ?? []) {
console.log(`Customer reply from ${message.from}: ${message.text?.body}`);
// Route to your support workflow or inbox
}
}
}
});
Template Examples
Create these templates in Dualhook's template management dashboard and wait for Meta approval before using them in your integration.
Order confirmation (order_confirmation):
Hi! Your order {{1}} for {{2}} {{3}} has been confirmed. We'll send you a tracking update when it ships.
Order shipped (order_shipped):
Good news! Your order {{1}} has shipped. Track your delivery here: {{2}}
Delivery reminder (delivery_reminder):
Hi! Your order {{1}} is out for delivery today. Someone should be available to receive the package.
For templates with media headers (product images, PDF invoices), see the Template Elements documentation.
Test with Dualhook
- Connect your WhatsApp number through Embedded Signup.
- Create and get approval for your order notification templates in the Dualhook dashboard.
- Register Shopify webhooks pointing to your server via the Shopify admin, the REST API, or the GraphQL Admin API.
- Place a test order in your Shopify store. Your server should receive the Shopify webhook and send the WhatsApp template.
- Check Dualhook's webhook activity logs for management event monitoring.
- Reply to the WhatsApp message from a test phone. The reply arrives directly at your server from Meta.
Your order notifications and customer replies flow between Meta and your server. Dualhook manages the WhatsApp configuration and operational monitoring — this routing separation is a Dualhook-specific configuration, not a Meta platform default.