Sync Shopify orders to message.com
Bring purchase history into the inbox. When a Shopify order is created or a customer is updated, push the data into the matching message.com Contact record. Agents see order count and lifetime value; AI grounds replies in actual purchase data.
When the Built-for-Shopify app ships, this is a one-click toggle inside the Shopify admin. Until then, this DIY webhook bridge is the equivalent. See Shopify plugin.
Architecture
- Shopify fires a webhook on
orders/create(and optionallyorders/updated,customers/update). - Your server verifies the HMAC signature.
- Your server finds or creates the matching contact on message.com by email, then
PATCHes the order data into itscustomFields.
1. Build the webhook handler
The handler verifies the signature, parses the order, and finds or creates the matching contact.
// Node / Express handler for the Shopify orders/create webhook
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.raw({ type: "application/json" }));
app.post("/webhooks/shopify/orders-create", async (req, res) => {
const hmac = req.get("X-Shopify-Hmac-Sha256")!;
const digest = crypto
.createHmac("sha256", process.env.SHOPIFY_WEBHOOK_SECRET!)
.update(req.body)
.digest("base64");
if (digest !== hmac) return res.status(401).end();
const order = JSON.parse(req.body.toString());
const email = order.email;
const customFields = {
shopifyCustomerId: order.customer?.id,
lastOrderId: order.id,
lastOrderTotal: Number(order.total_price),
lastOrderAt: order.created_at,
ordersCount: order.customer?.orders_count,
totalSpent: Number(order.customer?.total_spent),
};
const authHeader = { Authorization: `Bearer ${process.env.MESSAGE_WORKSPACE_JWT}` };
// There is no upsert endpoint: find-or-create by email, then set
// customFields. See the HubSpot sync tutorial for the same pattern.
const found = email
? await fetch(`https://app.message.com/api/v1/contacts?search=${encodeURIComponent(email)}`, { headers: authHeader })
.then(r => r.json())
.then(d => d.contacts.find((c) => c.email === email))
: null;
const contactId = found
? found.id
: await fetch("https://app.message.com/api/v1/contacts", {
method: "POST",
headers: { ...authHeader, "Content-Type": "application/json" },
body: JSON.stringify({ email }),
}).then(r => r.json()).then(d => d.contact.id);
await fetch(`https://app.message.com/api/v1/contacts/${contactId}`, {
method: "PATCH",
headers: { ...authHeader, "Content-Type": "application/json" },
body: JSON.stringify({ customFields }),
});
res.status(200).end();
});Verify the signature on the raw bytes before parsing JSON. Re-serialised JSON does not produce the same HMAC.
2. Register the webhook with Shopify
# Register the webhook via Shopify Admin API
curl -X POST \
-H "X-Shopify-Access-Token: shpat_xxx" \
-H "Content-Type: application/json" \
-d '{
"webhook": {
"topic": "orders/create",
"address": "https://your-server.com/webhooks/shopify/orders-create",
"format": "json"
}
}' \
https://your-store.myshopify.com/admin/api/2025-01/webhooks.jsonYou can also register from the Shopify admin under Settings → Notifications → Webhooks, but the API call is reproducible across environments.
3. Add useful topics
Register additional topics as your needs grow:
orders/updated. fulfillment status, edits.orders/cancelled. mark the order cancelled in custom fields.customers/create,customers/update. keep email and name fresh.refunds/create. flag refunded orders.
4. Verify
- Place a test order in Shopify.
- Watch your server logs for the inbound webhook.
- Open the matching contact in
app.message.com. The order data (count, total spent, last order) should appear in its custom fields. - Start a chat as that customer. Confirm the agent sees the custom fields on the contact.
Common pitfalls
- Retried webhooks re-running the sync. Shopify retries webhooks up to 19 times over 48 hours. There is no idempotency-key support on message.com's side, but this flow is naturally safe to re-run: find-then-create-or-update always converges on the same contact.
- Guest checkouts. Orders from anonymous customers have
customer = null. Fall back toorder.emailfor the contact lookup. - Webhook tolerance. Shopify expects a 2xx response within five seconds. If your upstream call to message.com is slow, ack the webhook first and enqueue the forward.
- Multi-store. If you run several Shopify stores against one message.com workspace, namespace external IDs (e.g.,
shopify-us:cust_123,shopify-eu:cust_123).
Next steps
- Install the widget on Shopify.
- Identify logged-in users with HMAC.
- Sync Stripe customers for non-Shopify revenue data.