WhatsApp + Shopify: 7 gotchas that cost me a week
I spent last month wiring WhatsApp order notifications into Shopify stores. The docs are decent. The gotchas aren’t documented anywhere. Here are the seven that actually cost me time. Signature verification needs the
I spent last month wiring WhatsApp order notifications into Shopify stores. The docs are decent. The gotchas aren’t documented anywhere.
Here are the seven that actually cost me time.
- Signature verification needs the raw body
This one burned a full day.
Meta signs every webhook with HMAC-SHA256 over the exact bytes it sent. If express.json() runs first, the raw body is gone — you’re hashing a re-serialised object, and the signature never matches.
`// ❌ Signature will never verify
app.use(express.json());
app.post('/webhooks/whatsapp', verifySignature, handler);
Mount the raw parser on that route before the global JSON parser:
`import crypto from 'node:crypto';
app.post(
'/webhooks/whatsapp',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.get('x-hub-signature-256') || '';
const expected =
'sha256=' +
crypto
.createHmac('sha256', process.env.META_APP_SECRET)
.update(req.body) // Buffer, not parsed JSON
.digest('hex');
const a = Buffer.from(sig);
const b = Buffer.from(expected);
// timingSafeEqual throws on length mismatch — check first
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
res.sendStatus(200); // ack fast
queue.add(JSON.parse(req.body.toString('utf8'))); // process after
}
);
app.use(express.json()); // everything else``
Two things in there matter beyond the signature: ack before you process, and use timingSafeEqual, not ===.
- Meta retries aggressively if you’re slow
Meta expects a 200 fast. Take too long and it retries — so your “send order confirmation” handler fires three times and the customer gets three messages.
Ack immediately, queue the work. Same for Shopify: it retries on non-2xx, so every handler needs to be idempotent. Dedupe on the event ID before you do anything with side effects.
- Shopify uses a different HMAC encoding
Both sign webhooks. They don’t agree on the format.
Header Encoding
Meta x-hub-signature-256 hex, prefixed sha256=
Shopify x-shopify-hmac-sha256 base64, no prefix
const digest = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET)
.update(req.body)
.digest('base64'); // not hex
Copy-pasting your Meta verifier for Shopify fails silently. Ask me how I know.
- The 24-hour window changes what you can send
This is the one that breaks product assumptions, not just code.
Customer messages you → 24-hour window opens → you can send free-form text
Window closes → template only, and templates need pre-approval
So “just reply to the customer” only works inside the window. Outside it, you need an approved template with variables filled in. Design your flows around this or you’ll ship something that works in testing (where you just messaged yourself) and fails in production.
- Template category decides your bill
Every template is marketing, utility, or authentication. Meta classifies it, and it drives the price per conversation.
In India the gap is large — marketing conversations cost several times what utility ones do. An order confirmation is utility. A discount blast is marketing. Mislabel a utility template as marketing and you pay more for the same message.
Rejections are usually one of:
Promotional language in a utility template
Unclear variables — {{1}} with no sample value
Variables at the very start or end of the body
A placeholder you forgot to replace
Submit with realistic sample values. “John” and “1042”, not “xxx”.
- Phone numbers must be E.164, and India trips people up
No +, no spaces, no dashes, country code always. 919876543210, not +91 98765 43210 and not 09876543210.
const toE164 = (raw, cc = '91') => {
const d = String(raw).replace(/\D/g, '');
if (d.length === 10) return cc + d; // bare local
if (d.startsWith('0')) return cc + d.slice(1); // leading zero
return d;
};
Normalise on the way in, not at send time. Otherwise the same customer ends up as three contacts.
- Opt-in isn’t legal boilerplate — it’s your delivery rate
Message people who didn’t opt in and they block you. Enough blocks and your number’s quality rating drops. Low quality means lower messaging limits, and Meta can restrict the number.
Record consent per customer with its source and timestamp. Treat it as operational data, not a compliance checkbox — it’s the thing protecting your ability to send at all.
{ customerId, phone, consent: true, source: 'checkout_opt_in', at: '2026-10-29T…' }
The order I’d build in
Shopify OAuth, store the token
Webhook endpoints with both HMAC verifiers, acking fast
Opt-in capture before any sending
One utility template (order confirmation), approved end to end
Then flows
Getting one template through approval before you build the flow engine saves a lot of rework.
Approval is the step with the unpredictable timeline.
I work on TopEdge, which does this for Shopify stores in India
we publish Meta’s India conversation rates if you want the current numbers for the template-category math in #5.
Everything above applies whether you build it or buy it.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.