HubSpot
The native HubSpot integration puts iMessage threads in the conversations inbox with two-way replies and contact sync; the API path below covers workflow-triggered sends.
The native integration
HubSpot does not need the API build below to see your messages. Connected natively, Blue Reacher puts iMessage threads into HubSpot's conversations inbox, and keeps the two systems matched:
- Inbox threads. Each iMessage conversation appears as a thread in the conversations inbox your team already works.
- Two-way replies. Replying from the HubSpot inbox sends a real iMessage from your line; the contact's reply lands back in the same thread.
- Contact sync. Contacts stay matched between the two systems by phone number.
Done-for-you setups get this connected during onboarding. The API path below is for the other direction: triggering sends from HubSpot workflows, which the inbox connection deliberately does not do on its own.
The API path: workflow-triggered sends
A workflow sends an iMessage when a contact hits any condition you can express in HubSpot. Replies come back as notes on the contact, with a timestamp property your lists and workflows can act on.
Before you start
Get your API key (start with a brk_test_ key; it simulates every send) and stand up an HTTPS endpoint for inbound replies. Register the endpoint with us and keep the signing secret server-side.
Outbound: workflow to Blue Reacher
With Operations Hub Professional or higher, use a webhook action.
- Create a contact-based workflow with your enrollment trigger.
- Add action: Send a webhook. Method POST, URL
https://api.bluereacher.com/v1/messages. - Add header
Authorizationwith valueBearer brk_your_api_key, andContent-Type: application/json. - Custom payload:
{
"to": "{{ contact.phone }}",
"message": "Hi {{ contact.firstname }}, it's Marcus from Northside. Saw you booked time with us. Anything you want covered before we talk?",
"metadata": { "crm_id": "{{ contact.hs_object_id }}" }
}Custom payloads and custom headers require Operations Hub Professional. Without it, use a Custom code action instead:
exports.main = async (event, callback) => {
const res = await fetch("https://api.bluereacher.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BLUEREACHER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
to: event.inputFields.phone,
message: `Hi ${event.inputFields.firstname}, quick question about your enquiry.`,
metadata: { crm_id: String(event.object.objectId) }
})
});
const data = await res.json();
callback({ outputFields: { queued: data.success ? "yes" : "no", messageId: data.message_id || "" } });
};Store the key as a secret in the custom code action, not inline. The custom-code route also lets you branch the workflow on the response, which the webhook action cannot do.
Phone number formatting
HubSpot stores phone numbers however they were entered. The API needs E.164. Either normalize on the way in with a calculated property, or handle it in the custom code action:
const e164 = raw.replace(/[^\d+]/g, "").replace(/^00/, "+");
const to = e164.startsWith("+") ? e164 : `+1${e164}`;Assuming +1 is fine for a US-only list and wrong for anything else. If you send internationally, store the country properly.
Inbound: replies onto the contact
Blue Reacher posts to your endpoint, and your endpoint writes to HubSpot. HubSpot cannot receive our webhook directly, since it needs a signature check and an API call back.
import crypto from "crypto";
export default async function handler(req, res) {
const raw = req.rawBody;
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.BLUEREACHER_WEBHOOK_SECRET)
.update(raw)
.digest("hex");
if (req.headers["x-bluereacher-signature"] !== expected) {
return res.status(401).json({ code: "invalid_signature" });
}
res.status(200).json({ received: true });
const e = JSON.parse(raw);
if (e.event !== "message.received") return;
const hsId = await hubspotIdForPhone(e.data.phone_number); // your stored mapping
await fetch("https://api.hubapi.com/crm/v3/objects/notes", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
properties: {
hs_note_body: `iMessage reply: ${e.data.content}`,
hs_timestamp: e.timestamp
},
associations: [
{
to: { id: hsId },
types: [{ associationCategory: "HUBSPOT_DEFINED", associationTypeId: 202 }]
}
]
})
});
}Set a custom contact property, last_imessage_reply, in the same handler. Active lists and workflows can trigger on it, which is how you get "notify the owner when a lead replies" without polling.
Private app scopes
Create a private app in HubSpot with crm.objects.contacts.read, crm.objects.contacts.write, and crm.objects.notes.write. Use its token in the handler above.
Suppress on opt-out
Subscribe to contact.opted_out on your webhook endpoint and set a HubSpot property, imessage_opted_out, to true on the matching record. Then add that property as an exclusion on every messaging workflow. The platform refuses sends to opted-out contacts anyway, but keeping HubSpot in sync stops your team from working a record they should leave alone. See opt-out handling.
Checklist
- Phone numbers normalized to E.164
- Key stored as a secret, never in a payload template
-
hs_object_idstamped into contactcustom_fields/ your phone-to-record map - Signature verified before the note is written
-
200returned before the HubSpot call, not after - Opt-out property syncing and excluded from every workflow
Integrations overview
Every way to connect Blue Reacher to the tools you already run: native GoHighLevel, per-platform setup guides, no-code automation, and the raw API for anything else.
Salesforce
The native Salesforce integration puts iMessage threads on Contact and Lead timelines with an embedded reply panel; the Flow build below covers record-triggered sends.

