Skip to the page

Guides

Text new Follow Up Boss leads at once and log every text

Text each new lead in Follow Up Boss from your own number within a minute, then log every text in and out on the person in Follow Up Boss, so the whole team sees the conversation.

Jordan is an agent at Harbor Realty. Leads from Zillow, the brokerage site and open houses all arrive in Follow Up Boss, and the agent who texts first usually gets the client. Jordan texts from his own iPhone because people answer a real number, but then the conversation lives on his phone and his lender partner and his assistant can’t see it. With this guide, every new lead gets a text from Jordan’s own number within a minute, and every text in and out with that person is logged on their Follow Up Boss record.

What you’ll have

  • A text from your own number to each new lead within a minute of them arriving in Follow Up Boss.
  • Every text in and out logged on the person in Follow Up Boss, in the same list as the texts your team sends there.
  • A note on the person when they text STOP, so nobody texts them by hand.

What you need

  • A BlueBot account with your Mac connected.
  • A Follow Up Boss account with API access (an owner or admin can find the API key under Admin → API).
  • A Zapier account for the first part.
  • For the logging: somewhere to run a small web server with an https address, and Node.js 18 or later or Python 3.10 or later.

Part 1: text new leads right away

Start the Zap

In Zapier, make a Zap that starts with Follow Up Boss and its trigger for a new person, and connect your Follow Up Boss account. Load a sample so you can see the fields.

Add Filter by Zapier so only leads worth a text go on: the person has a phone number, and their source is one you want to text (for example, Source contains “Zillow” or “Website”). A lead from a source that didn’t ask to be texted, such as a bought list, should not get one.

Add BlueBot’s Send Text action

Add BlueBot’s Send Text action and connect BlueBot with an API key that has the messages:send scope (make one in Developers → API keys, named “Zapier”). Then fill in:

BlueBot fieldMap it to
ToThe person’s phone from Follow Up Boss. US numbers can leave off the +1.
TextYour words with the person’s first name mapped in: “Hi Ana, this is Jordan with Harbor Realty. I saw your request come in. Would you like to see some homes this week?”
Metadatafub_person_id mapped to the person’s ID, and source set to follow-up-boss.
ServiceAuto, the default: iMessage when they use it, otherwise SMS through your iPhone.

BlueBot’s Zapier app sends an idempotency key built from the Zap and the step, so when Zapier retries a step, the lead still gets one text.

Test it with your own number

Put your own number in To for the test and press Test step. A text arrives on your phone from your own number, and Zapier shows the message BlueBot returned with state: "queued". Map the person’s phone back into To and turn the Zap on.

You gotIt meansDo next
A text on your phone, and state: "queued" in ZapierIt works.Turn the Zap on.
state: "queued" with a holdThe text is waiting, most often for quiet hours at night.It goes on its own when the hold ends. Holds
state: "canceled"Your number is on your own stop list.Take it off in Contacts and test again.
An error naming toThe phone field was empty or not a number.Keep the filter that requires a phone.

A lead that arrives at 11 PM is accepted and held until your quiet hours end, so it gets its text at 8 AM rather than in the middle of the night. A lead from out of state is held by their own clock when you turn on Smart quiet hours. Quiet hours

Part 2: log every text on the person

Follow Up Boss lets an outside system log a text on a person with its textMessages endpoint. Your server hears about each text from BlueBot (message.sent for each one out, including those you type by hand on your iPhone, and message.received for each reply), finds the person by phone number and logs it.

Make a BlueBot webhook

In Developers → Webhooks, make a webhook for your server with message.sent, message.received and contact.stopped ticked, or:

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://harborrealty.example/bluebot/webhooks",
    "events": [
      "message.sent",
      "message.received",
      "contact.stopped"
    ],
    "description": "Log texts in Follow Up Boss"
  }'

Copy the signing secret from the answer; it’s shown once. Follow Up Boss asks integrations to name themselves in an X-System header; register your system with Follow Up Boss to get its X-System-Key.

Terminal
export BLUEBOT_API_KEY="bb_live_…"
export BLUEBOT_WEBHOOK_SECRET="whsec_…"
export FUB_API_KEY="…"
export FUB_SYSTEM="Harbor-Realty-Texts"
export FUB_SYSTEM_KEY="…"

Write the handler

The handler finds the person by the metadata the Zap sent (fub_person_id) when there is one, and otherwise by phone number. It logs the text with isIncoming set for replies, and adds a note when someone stops. It skips group chats and remembers the event ids it handled, so a retried delivery never logs a text 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 FUB = 'https://api.followupboss.com/v1';
const handled = new Set<string>(); // event ids; use a table in production
const personByNumber = new Map<string, number>(); // use a table in production

async function fub(path: string, init: RequestInit = {}) {
  const response = await fetch(`${FUB}${path}`, {
    ...init,
    headers: {
      Authorization: `Basic ${Buffer.from(`${process.env.FUB_API_KEY}:`).toString('base64')}`,
      'X-System': process.env.FUB_SYSTEM!,
      'X-System-Key': process.env.FUB_SYSTEM_KEY!,
      'Content-Type': 'application/json',
    },
  });
  if (!response.ok) throw new Error(`Follow Up Boss answered ${response.status}`);
  return response.json();
}

async function findPerson(number: string): Promise<number | null> {
  const known = personByNumber.get(number);
  if (known) return known;
  const tenDigits = number.replace(/\D/g, '').slice(-10);
  const result = await fub(`/people?phone=${tenDigits}&fields=id&limit=2`);
  if (result.people?.length !== 1) return null; // nobody, or two people share it: don't guess
  personByNumber.set(number, result.people[0].id);
  return result.people[0].id;
}

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.sent' || event.type === 'message.received') && !event.data.chatId) {
      const message = event.data;
      const incoming = message.direction === 'in';
      const theirNumber = (incoming ? message.from : message.to)!;
      if (message.metadata.fub_person_id) personByNumber.set(theirNumber, Number(message.metadata.fub_person_id));
      const personId = await findPerson(theirNumber);
      if (personId) {
        await fub('/textMessages', {
          method: 'POST',
          body: JSON.stringify({
            personId,
            message: message.text ?? `(sent ${message.files.length} file${message.files.length === 1 ? '' : 's'})`,
            toNumber: message.to,
            fromNumber: message.from,
            isIncoming: incoming,
          }),
        });
      }
    }
    if (event.type === 'contact.stopped') {
      const personId = await findPerson(event.data.handle);
      if (personId) {
        await fub('/notes', {
          method: 'POST',
          body: JSON.stringify({ personId, subject: 'Stopped texting', body: 'Texted STOP. BlueBot won’t text them until they text START.' }),
        });
      }
    }
    handled.add(event.id);
    res.status(204).end();
  } catch (error) {
    console.error('Could not log in Follow Up Boss', event.id, error instanceof Error ? error.message : error);
    res.status(500).end(); // BlueBot tries again
  }
});

app.listen(3000);

Logged texts appear on the person in Follow Up Boss beside the texts sent from Follow Up Boss itself. They don’t start Follow Up Boss’s automations or count as a call.

Test it with your own number

  1. Add yourself as a person in Follow Up Boss with your own mobile number, from a source your Zap’s filter lets through.
  2. The Zap texts you within a minute or two (Zapier checks Follow Up Boss on your plan’s schedule).
  3. Reply from a phone with a different number than the Mac’s, then open yourself in Follow Up Boss.
You gotIt meansDo next
Both texts on the person, the reply marked incomingIt works.Leave it running.
The text out is logged, the reply isn’tBlueBot didn’t copy the reply, often because the phone you replied from shares an Apple ID with the Mac.Reply from a second phone of your own.
Neither is logged, deliveries show 204The person wasn’t found by phone, or two people share the number.Check the phone on the person. Texts sent by the Zap carry fub_person_id and always match.
Deliveries show 500, your log says 401The API key or the X-System headers are wrong.Copy the key from Admin → API again.
Deliveries show 400The signature didn’t match.Read the raw body, and use this webhook’s secret. Verify signatures

When it goes wrong

  • A lead got two texts. Two Zaps start on new people, or Follow Up Boss’s own auto-text also fires. Keep one.
  • A lead never got a text. Look the text up in Messages in the BlueBot dashboard by their number. A hold says why it’s waiting, and a canceled text with cancel_reason: "opted_out" means they’re on your stop list.
  • Texting many new leads a day. BlueBot limits how many people you’ve never texted can be texted in a day (50 by default), because texting many strangers is what gets an Apple account flagged. Leads past the limit wait for tomorrow with the hold daily_new_people_limit. Raise it carefully in Settings → Sending rules. Sending rules
  • Your server was down. Deliveries retry for about 3 days. For longer, catch up from GET /v1/events (Replay events).

Next