# 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 field | Map it to                                                                                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| To            | The person’s phone from Follow Up Boss. US numbers can leave off the +1.                                                                                                |
| Text          | Your 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?” |
| Metadata      | `fub_person_id` mapped to the person’s ID, and `source` set to `follow-up-boss`.                                                                                        |
| Service       | Auto, 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 got                                               | It means                                                  | Do next                                                              |
| ----------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- |
| A text on your phone, and `state: "queued"` in Zapier | It works.                                                 | Turn the Zap on.                                                     |
| `state: "queued"` with a `hold`                       | The text is waiting, most often for quiet hours at night. | It goes on its own when the hold ends. [Holds](https://usebluebot.com/docs/concepts/holds) |
| `state: "canceled"`                                   | Your number is on your own stop list.                     | Take it off in **Contacts** and test again.                          |
| An error naming `to`                                  | The 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](https://usebluebot.com/docs/concepts/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:

```bash
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"
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const webhook = await client.webhooks.create({
  url: 'https://harborrealty.example/bluebot/webhooks',
  events: ['message.sent', 'message.received', 'contact.stopped'],
  description: 'Log texts in Follow Up Boss',
});
console.log(webhook);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

webhook = client.webhooks.create(
    url="https://harborrealty.example/bluebot/webhooks",
    events=["message.sent", "message.received", "contact.stopped"],
    description="Log texts in Follow Up Boss",
)
print(webhook)
```

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`.

```bash
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).

```ts title="TypeScript"
// 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);
```

```python title="Python"
# server.py: pip install flask requests bluebot
import os
import re

import requests
from flask import Flask, abort, request
from bluebot import BlueBot, WebhookVerificationError

app = Flask(__name__)
bluebot = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])
FUB = "https://api.followupboss.com/v1"
handled: set[str] = set()  # event ids; use a table in production
person_by_number: dict[str, int] = {}  # use a table in production

def fub(method: str, path: str, **kwargs) -> dict:
    response = requests.request(
        method,
        f"{FUB}{path}",
        auth=(os.environ["FUB_API_KEY"], ""),
        headers={"X-System": os.environ["FUB_SYSTEM"], "X-System-Key": os.environ["FUB_SYSTEM_KEY"]},
        timeout=10,
        **kwargs,
    )
    response.raise_for_status()
    return response.json()

def find_person(number: str) -> int | None:
    if number in person_by_number:
        return person_by_number[number]
    ten_digits = re.sub(r"\D", "", number)[-10:]
    people = fub("GET", "/people", params={"phone": ten_digits, "fields": "id", "limit": 2}).get("people", [])
    if len(people) != 1:  # nobody, or two people share it: don't guess
        return None
    person_by_number[number] = people[0]["id"]
    return people[0]["id"]

@app.post("/bluebot/webhooks")
def bluebot_webhook():
    try:
        event = bluebot.webhooks.verify(
            request.get_data(), request.headers, secret=os.environ["BLUEBOT_WEBHOOK_SECRET"]
        )
    except WebhookVerificationError:
        abort(400)
    if event.id in handled:
        return "", 204

    try:
        if event.type in ("message.sent", "message.received") and not event.data.chat_id:
            message = event.data
            incoming = message.direction == "in"
            their_number = message.from_ if incoming else message.to
            if message.metadata.get("fub_person_id"):
                person_by_number[their_number] = int(message.metadata["fub_person_id"])
            person_id = find_person(their_number)
            if person_id:
                fub(
                    "POST",
                    "/textMessages",
                    json={
                        "personId": person_id,
                        "message": message.text or f"(sent {len(message.files)} file(s))",
                        "toNumber": message.to,
                        "fromNumber": message.from_,
                        "isIncoming": incoming,
                    },
                )
        if event.type == "contact.stopped":
            person_id = find_person(event.data.handle)
            if person_id:
                fub(
                    "POST",
                    "/notes",
                    json={
                        "personId": person_id,
                        "subject": "Stopped texting",
                        "body": "Texted STOP. BlueBot won’t text them until they text START.",
                    },
                )
        handled.add(event.id)
        return "", 204
    except requests.RequestException as error:
        app.logger.error("Could not log in Follow Up Boss: %s %s", event.id, error)
        return "", 500  # BlueBot tries again
```

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 got                                             | It means                                                                                                 | Do next                                                                                      |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Both texts on the person, the reply marked incoming | It works.                                                                                                | Leave it running.                                                                            |
| The text out is logged, the reply isn’t             | BlueBot 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 `204`            | The 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 `401`          | The API key or the `X-System` headers are wrong.                                                         | Copy the key from **Admin → API** again.                                                     |
| Deliveries show `400`                               | The signature didn’t match.                                                                              | Read the raw body, and use this webhook’s secret. [Verify signatures](https://usebluebot.com/docs/webhooks/verify) |

## 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](https://usebluebot.com/docs/concepts/sending-rules)
- **Your server was down.** Deliveries retry for about 3 days. For longer, catch up from `GET /v1/events` ([Replay events](https://usebluebot.com/docs/webhooks/replay)).

## Next

- [Follow Up Boss recipes](https://usebluebot.com/docs/recipes/follow-up-boss): The new-lead text, the reply log and a text to yourself for each new lead.
- [HubSpot timeline](https://usebluebot.com/docs/guides/hubspot-timeline): The same logging for HubSpot, with a match by record ID.
- [Drip that stops on reply](https://usebluebot.com/docs/guides/drip-sequence): Follow up with a lead who doesn’t answer, and stop when they do.
- [Metadata](https://usebluebot.com/docs/concepts/metadata): Carry your CRM’s ids on a text and get them back on every webhook.
