The pitch is obvious: your CRM knows who your customers are and what stage they are at, so it should be able to text them. The half-built version is equally common — outbound works, inbound is lost, and within a month your team is answering replies on someone's personal phone while the CRM shows a one-sided conversation.
The integration has four parts, not one
Most teams build the first and discover the other three in production.
- Outbound. A CRM workflow triggers a message. Easy, and where everyone starts.
- Inbound. Replies arrive on a webhook and must be written to the right contact record. This is the part that gets skipped.
- Identity resolution. A reply arrives from
+15551234567. Which contact is that? Phone formats in CRMs are inconsistent enough that naive matching fails often. - Timeline sync. Both directions logged against the contact, so anyone opening the record sees the whole conversation.
Normalise phone numbers before you match on them
(555) 123-4567, 555-123-4567 and +15551234567 are the same person and three different strings. Normalise to E.164 on write, index that column, and match on it. Doing this later means backfilling every contact you have.
Identity resolution that actually works
import { parsePhoneNumber } from "libphonenumber-js"; export function normalise(raw: string, defaultCountry = "US") { try { const parsed = parsePhoneNumber(raw, defaultCountry); return parsed?.isValid() ? parsed.format("E.164") : null; } catch { return null; }} export async function resolveContact(from: string) { const e164 = normalise(from); if (!e164) return null; // Exact match on the normalised column is the only reliable lookup. const contact = await crm.contacts.findByPhone(e164); if (contact) return contact; // A reply from an unknown number is a real event, not an error — someone // forwarded your text, or the contact is under a second number. return await crm.contacts.create({ phone: e164, source: "imessage_inbound", // Flag rather than guess. A human decides whether to merge. needsReview: true, });}Writing both directions to the timeline
The value of the integration is the record, not the send. If a salesperson opens a contact and sees only what was sent, the integration has failed at its actual job.
export async function logOutbound(contactId: string, text: string, messageId: string) { await crm.engagements.create({ contactId, type: "message", direction: "outbound", channel: "imessage", body: text, externalId: messageId, // lets a later delivery event find this row occurredAt: new Date().toISOString(), });} export async function logInbound(contactId: string, text: string, messageId: string) { await crm.engagements.create({ contactId, type: "message", direction: "inbound", channel: "imessage", body: text, externalId: messageId, occurredAt: new Date().toISOString(), }); // A reply is a signal, not just a record. Route it to whoever owns the deal. await crm.tasks.create({ contactId, title: "Replied by text", assigneeId: await crm.contacts.ownerOf(contactId), dueAt: new Date(Date.now() + 15 * 60_000).toISOString(), });}The major CRMs, specifically
| CRM | Native iMessage? | How the integration works |
|---|---|---|
| GoHighLevel | No — but Linq publishes a GHL integration | Trigger sends from GHL workflows; map replies back through webhooks |
| HubSpot | No | Custom timeline events via the CRM API; sends triggered by workflow webhooks |
| Salesforce | No | Custom object or Task records; Platform Events for inbound if you want realtime |
| Pipedrive | No | Activities API for the timeline; webhooks both directions |
| Anything else | No | The provider's webhook plus the CRM's REST API. The pattern above is portable |
Speed is the whole point
A CRM-triggered text that goes out within a minute of a form fill converts dramatically better than one sent in the next batch. If you build only one automation, build that one — speed to lead has the numbers.
What to check before you commit
- Does the provider push inbound to a webhook, or do you have to poll? Polling makes conversational use unpleasant.
- Does it send delivery events you can attach to the timeline entry you already wrote?
- Can you send from multiple lines, so a rep's messages come from their own number rather than a shared one?
- Does opt-out propagate to your CRM, or only to the provider's suppression list? If only theirs, your workflows will keep trying.
- Can you tag links per campaign so the CRM shows what a contact actually clicked? The UTM builder generates them.
For the sales motion built on top of this, see iMessage for B2B sales; for two-way conversation design, two-way texting.
Common questions
- Which iMessage API integrates with GoHighLevel?
- Linq publishes a GoHighLevel integration for its Linq Blue product, letting you send blue-bubble messages from GHL workflows. Other providers generally integrate through webhooks and the GHL API rather than a native app.
- Can I send iMessages from HubSpot or Salesforce?
- Not natively — neither has an iMessage channel. You integrate through an iMessage provider's API, writing sends and replies back to the contact timeline yourself or through the provider's integration if one exists.