message.comDevelopers

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

  1. Shopify fires a webhook on orders/create (and optionally orders/updated, customers/update).
  2. Your server verifies the HMAC signature.
  3. Your server finds or creates the matching contact on message.com by email, then PATCHes the order data into its customFields.

1. Build the webhook handler

The handler verifies the signature, parses the order, and finds or creates the matching contact.

server.ts
// 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

bash
# 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.json

You 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

  1. Place a test order in Shopify.
  2. Watch your server logs for the inbound webhook.
  3. Open the matching contact in app.message.com. The order data (count, total spent, last order) should appear in its custom fields.
  4. 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 to order.email for 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