Skip to the page

Guides

Text from HighLevel workflows through your own number

Send the texts your HighLevel workflows already send from your own number through BlueBot instead of a rented one, then put every reply on the contact and tag the ones who text STOP.

Alex runs Summit Fitness, a small gym, and runs his marketing in GoHighLevel. His workflows text every new sign-up and every booked intro session, but they text from a number HighLevel rents him, one his members have never seen. Half of them don’t answer a number they don’t know. With this guide, the same workflows text from Alex’s own phone number through BlueBot, the texts appear in his existing threads with his members, and each reply is added to the contact in HighLevel as a note.

What you’ll have

  • A HighLevel workflow action that sends a text from your own number and fills in the contact’s fields.
  • Each reply added to the contact in HighLevel as a note, so your team sees it with the rest of their history.
  • The tag stopped-texting on any contact who texts STOP, so a workflow can skip them.

What you need

  • A BlueBot account with your Mac connected.
  • A HighLevel sub-account where you can edit workflows. The first part needs nothing else.
  • For the replies: a HighLevel Private Integration token for that sub-account, its Location ID, and somewhere to run a small web server with an https address. Node.js 18 or later, or Python 3.10 or later.

Part 1: send from a workflow

HighLevel workflows have a Webhook action that posts the contact’s details to any address. A BlueBot Text Hook is an address that turns each post it gets into a text, so the workflow needs no API key and no code. Text Hooks are set up in the dashboard; they have no API yet.

Make a Text Hook

In the BlueBot dashboard, open Text Hooks and press New hook. Name it after the workflow (“New member welcome”) and save. Copy the hook’s address: https://usebluebot.com/h/ followed by a long token. Treat it like a password, because anyone with it can make your hook text somebody.

Add the Webhook action to your workflow

In HighLevel, open Automation, then Workflows, and open the workflow that should text, such as the one that starts when a contact is created. Add an action, choose Webhook, set the method to POST and paste the hook’s address as the URL.

If the workflow has a Send SMS action for the same text, delete it, or every contact gets two texts from two numbers. Leave HighLevel’s other actions where they are.

Run the workflow once with yourself

Make a contact in HighLevel with your own first name and your own mobile number, and let the workflow run for it (add it to the workflow by hand if it doesn’t start on its own).

Back on the hook’s page in BlueBot, the test panel lists the last 20 posts by their field names. BlueBot never shows or keeps the values there, only the names. A HighLevel post carries the contact at the top level, with names such as first_name, phone, email and contact_id.

Map the recipient and write the text

Set Recipient to the phone field. Press a field in the test panel to put its path into the template; a template reads the post under body:

Alex’s welcome template
Hi {{body.first_name}}, it’s Alex from Summit Fitness. Thanks for signing up. Want to book a free intro session this week?

Under Metadata, add ghl_contact_id set to {{body.contact_id}}. BlueBot echoes metadata on every webhook about the text, so anything you build on those webhooks later knows which HighLevel contact a text was for.

Turn on Create a contact and add the tag highlevel, then save the hook.

Test it with your own number

Run the workflow for your own contact again. A text arrives on your phone from your own number within a few seconds, and the hook’s page lists the post.

You gotIt meansDo next
A text on your phone from your numberIt works.Turn the workflow on for everyone.
The post is listed but no text cameThe recipient field was empty or not a phone number, or the text is waiting with a hold.Open the text in Messages in the dashboard and read its state and hold. Quiet hours hold texts at night. Holds
No post on the hook’s pageHighLevel didn’t reach the hook.Check the workflow’s execution log in HighLevel for the Webhook action, and that the URL was pasted whole.
Two texts, one from another numberHighLevel’s own Send SMS action is still in the workflow.Delete it.

Every text a hook sends meets your sending rules like any other: pacing, quiet hours, your daily limit of new people and the stop list. A workflow that fires for 300 contacts at once doesn’t send 300 texts at once; they leave your Mac one every 20 seconds by default. Sending rules

Part 2: put replies on the contact

BlueBot tells your server about each reply with a message.received webhook. The server finds the contact in HighLevel by the sender’s number and adds the reply as a note. When someone texts STOP, BlueBot sends contact.stopped, and the server tags the contact.

Make a Private Integration in HighLevel

In the sub-account, open Settings, then Private Integrations, and create one named “BlueBot replies” with the scopes View Contacts and Edit Contacts (contacts.readonly and contacts.write). Copy its token. Copy the sub-account’s Location ID from Settings → Business Profile too.

Make a BlueBot webhook

In BlueBot, open Developers, then Webhooks, and make one for your server with message.received and contact.stopped ticked, or with the API:

POST/v1/webhooksMake a webhook →
curl -X POST "https://usebluebot.com/api/v1/webhooks" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://summitfitness.example/bluebot/webhooks",
    "events": [
      "message.received",
      "contact.stopped"
    ],
    "description": "Replies to HighLevel"
  }'

Copy the signing secret from the answer; it’s shown once.

Terminal
export BLUEBOT_API_KEY="bb_live_…"
export BLUEBOT_WEBHOOK_SECRET="whsec_…"
export GHL_TOKEN="pit-…"
export GHL_LOCATION_ID="…"

Write the handler

HighLevel’s API finds a contact by phone number with its duplicate search, which matches a number however it was typed. The handler adds the reply as a note and tags anyone who stopped. It skips group chats, and it keeps the event ids it has handled so a retried delivery never adds a note twice (in memory here; use a table in production).

// server.ts: npm install express @usebluebot/sdk
import express from 'express';
import BlueBot, { WebhookVerificationError } from '@usebluebot/sdk';

const bluebot = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });
const GHL = 'https://services.leadconnectorhq.com';
const handled = new Set<string>(); // event ids; use a table in production

async function ghl(path: string, init: RequestInit = {}) {
  const response = await fetch(`${GHL}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.GHL_TOKEN}`,
      Version: '2021-07-28',
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
  });
  if (!response.ok) throw new Error(`HighLevel answered ${response.status}`);
  return response.json();
}

async function findContact(number: string): Promise<string | null> {
  const query = new URLSearchParams({ locationId: process.env.GHL_LOCATION_ID!, number });
  const result = await ghl(`/contacts/search/duplicate?${query}`);
  return result.contact?.id ?? null;
}

const app = express();

app.post('/bluebot/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = bluebot.webhooks.verify(req.body.toString('utf8'), req.headers, process.env.BLUEBOT_WEBHOOK_SECRET!);
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.status(400).send('Bad signature');
    throw error;
  }
  if (handled.has(event.id)) return res.status(204).end();

  try {
    if (event.type === 'message.received' && !event.data.chatId) {
      const message = event.data;
      const contactId = await findContact(message.from!);
      if (contactId) {
        const words = message.text ?? `(sent ${message.files.length} file${message.files.length === 1 ? '' : 's'})`;
        await ghl(`/contacts/${contactId}/notes`, {
          method: 'POST',
          body: JSON.stringify({ body: `Text from ${message.from}: ${words}` }),
        });
      }
    }
    if (event.type === 'contact.stopped') {
      const contactId = await findContact(event.data.handle);
      if (contactId) {
        await ghl(`/contacts/${contactId}/tags`, { method: 'POST', body: JSON.stringify({ tags: ['stopped-texting'] }) });
      }
    }
    handled.add(event.id);
    res.status(204).end();
  } catch (error) {
    console.error('Could not update HighLevel', event.id, error instanceof Error ? error.message : error);
    res.status(500).end(); // BlueBot tries again
  }
});

app.listen(3000);

The log lines name the event and never the words of a text. Keep it that way.

Skip stopped contacts in your workflows

Add an If/Else to the start of each workflow that texts: when the contact has the tag stopped-texting, end the workflow. BlueBot would cancel the text anyway, since the person is on your stop list, but skipping them keeps canceled texts out of the workflow’s history.

Test it with your own number

Reply to the welcome text you sent yourself in part 1, from your phone. Open your contact in HighLevel.

You gotIt meansDo next
A note “Text from +1…” with your replyIt works.Leave the server running; every reply is now added to the contact.
No note, and the webhook’s deliveries in Developers → Webhooks show 204The duplicate search found nobody with that number in this sub-account.Check the contact’s phone, and that GHL_LOCATION_ID is this sub-account’s.
Deliveries show 500 and your log says HighLevel answered 401The token is wrong or for another sub-account.Copy the Private Integration’s token again.
Deliveries show 400The signature didn’t match.Read the raw body before parsing it, and use this webhook’s secret. Verify signatures
No deliveries at allBlueBot didn’t copy your reply. Your phone may share an Apple ID with the Mac.Reply from a second phone of your own with a different number.

When it goes wrong

  • Members still get texts from the old number. A workflow still has a Send SMS action, or a campaign outside workflows sends them. Search your workflows for Send SMS.
  • A text never went. Open it in Messages in the BlueBot dashboard. A hold says why it’s waiting (quiet hours, your Mac offline, the day’s limit of new people), and a failed text says why it didn’t go. Message states
  • A big workflow takes hours. That’s pacing: one text every 20 seconds from your Mac, the way a person sends them. Spread a large blast over the day, or send it as a broadcast from the dashboard, which tells you when the last text will go. Sending rules
  • Replies from people the workflow never texted aren’t noted. BlueBot copies texts only from people you’ve texted through it, unless you set Which chats to copy to Everything. Receiving texts

Next