Sync HubSpot contacts to message.com
Mirror HubSpot contact records into message.com visitor records. Lifecycle stage, lead status, owner, and any custom property you choose. The inbox shows where each visitor sits in the funnel.
1. Create a HubSpot private app
- In HubSpot, go to Settings → Integrations → Private Apps.
- Click Create a private app.
- Grant scopes:
crm.objects.contacts.read,crm.schemas.contacts.read. - Copy the access token into
HUBSPOT_TOKEN. - From the same app page, copy the client secret into
HUBSPOT_APP_SECRETfor signature verification.
2. Subscribe to webhooks
- Open the private app's Webhooks tab.
- Set the target URL to your HTTPS endpoint.
- Subscribe to
contact.creationandcontact.propertyChange. For property changes, pickemail,firstname,lastname,lifecyclestage,hs_lead_status, and any custom property you want pushed. - Save and activate.
3. Build the handler
server.ts
// Node / Express handler for HubSpot webhooks
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.raw({ type: "application/json" }));
app.post("/webhooks/hubspot", async (req, res) => {
const sig = req.get("X-HubSpot-Signature-v3")!;
const timestamp = req.get("X-HubSpot-Request-Timestamp")!;
const base = "POST" + req.protocol + "://" + req.get("host") + req.originalUrl + req.body + timestamp;
const computed = crypto
.createHmac("sha256", process.env.HUBSPOT_APP_SECRET!)
.update(base)
.digest("base64");
if (computed !== sig) return res.status(401).end();
const events: Array<{ objectId: number; eventId: number; propertyName?: string; propertyValue?: string }>
= JSON.parse(req.body.toString());
for (const evt of events) {
const contact = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts/${evt.objectId}?properties=email,firstname,lastname,lifecyclestage,hs_lead_status`,
{ headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}` } }
).then(r => r.json());
await fetch("https://app.message.com/api/v1/visitors/upsert", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MESSAGE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": String(evt.eventId),
},
body: JSON.stringify({
email: contact.properties.email,
name: `${contact.properties.firstname ?? ""} ${contact.properties.lastname ?? ""}`.trim(),
externalId: `hubspot:${evt.objectId}`,
attributes: {
lifecycleStage: contact.properties.lifecyclestage,
leadStatus: contact.properties.hs_lead_status,
},
}),
});
}
res.status(200).end();
});HubSpot batches events, so each delivery is an array. Loop, fetch the contact, upsert. Use the event ID as the idempotency key.
4. Verify
- Change a HubSpot contact's lifecycle stage to
customer. - Watch your server logs.
- Open the matching visitor in
app.message.com. ConfirmlifecycleStage: customerin attributes.
Common pitfalls
- Bulk imports. Importing 10k contacts into HubSpot fires 10k webhooks. Add a small queue (Redis, BullMQ, SQS) between your endpoint and the message.com upsert to smooth the burst.
- Email-less contacts. Some HubSpot contacts have no email (form fills, anonymous tracking). Upsert by external ID anyway; let the email backfill on the next event.
- HubSpot merges. When two contacts merge, you get a
contact.mergeevent. The losing record's ID becomes stale. Listen and delete or merge in message.com. - Two-way sync. This tutorial pushes HubSpot to message.com. Pushing the other direction (chat events to HubSpot timeline) requires the HubSpot Engagements API. See OAuth apps when that ships.
Next steps
- Identify logged-in users with HMAC.
- Sync Salesforce leads.
- Send proactive messages based on lifecycle stage.