# Text everyone who fills in your Typeform or Jotform

> Text each person who fills in your Typeform or Jotform from your own number within a minute, with a Text Hook and no code, or with a small webhook handler when you want your own logic.

Dana runs Brightside Remodeling. Her website has a Typeform called “Get a quote”, and by the time she calls people back the next morning, half of them have hired someone else. With this guide, everyone who asks for a quote gets a text from Dana’s own number within a minute, and their reply shows up in her inbox and on her iPhone like any other text.

## What you’ll have

- A text from your number to each person who fills in your form, within a minute of them pressing Submit.
- The person added to your contacts and tagged, so you can follow up or add them to a sequence later.
- Their reply in BlueBot’s inbox, in Messages on your Mac and on your iPhone.

## What you need

- A BlueBot account with your Mac connected. If **Home** in the dashboard doesn’t show your Mac as online, open **Get started** first.
- A Typeform or Jotform form you can edit.
- A line on the form that tells people you’ll text them. Text only people who asked to hear from you.

There are two ways to build it. A Text Hook takes ten minutes and no code. The API way is a small webhook handler, for when you want to choose who gets a text, write different texts for different answers, or save the reply somewhere else too.

## The no-code way: a Text Hook

A Text Hook is an address at BlueBot that turns each post it gets into a text. Typeform and Jotform post each new entry to an address you give them, so the hook sits between the two. Text Hooks are set up in the dashboard; they have no API yet.

### Ask for a mobile number

In Typeform, add a **Phone Number** question, make it required and set its default country to the United States. In Jotform, add a **Phone** field and make it required.

Put the permission line in the question’s description, so it sits beside the number: “By sharing your number you agree that Brightside Remodeling may text you about your quote.”

Give the question for the person’s first name a clear title, such as “First name”. You’ll pick it by its title in a minute.

### Make the Text Hook

In the BlueBot dashboard, open **Text Hooks** and press **New hook**. Name it after the form (“Quote form”) and pick the **Typeform** or **Jotform** preset. The preset knows how that app shapes its posts, so it can find the phone number and name answers for you.

Copy the hook’s address. It’s `https://usebluebot.com/h/` followed by a long token. Treat it like a password: anyone who has it can make your hook text somebody.

### Point the form at the hook

In **Typeform**, open the form, go to **Connect**, then **Webhooks**, press **Add a webhook**, paste the hook’s address and save. Turn the webhook on with its switch.

In **Jotform**, open the form, go to **Settings**, then **Integrations**, search for **WebHooks**, paste the hook’s address and press **Complete Integration**.

> **Don’t use the form’s test button.** Typeform’s **Send test request** posts sample answers, and a sample phone number belongs to somebody. Once your hook is mapped, a test request would text them. Test by filling in the form yourself, as below.

### Fill in the form yourself

Open the form’s public link and fill it in with your own mobile number and your own first name. This post is how the hook learns the shape of your form.

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 in that panel, only the names, so you pick fields without anyone’s answers on screen.

### Map the recipient and write the text

Set **Recipient** to the phone number answer. The preset picks it when the form has one phone question; check that it did.

Write the text. Press a field in the test panel to put its path into the template where your cursor is. A template reads the post under `body`, so a first-name field looks like `{{body.first_name}}` in the template, although Typeform’s and Jotform’s answers sit deeper in the post than that example. Let the picker write the path for you.

```text title="Dana’s template"
Hi {{body.first_name}}, this is Dana at Brightside Remodeling. Thanks for asking for a quote. What’s a good time for a quick call this week?
```

Keep it short and personal. A text that reads like a person wrote it gets answered; one that reads like an ad gets reported, and reports are what get an Apple account flagged.

### Add the person to your contacts

Turn on **Create a contact** and add the tag `quote-form`. Each person who fills in the form becomes a contact with that tag, so you can see who came from the form and add them to a [sequence](https://usebluebot.com/docs/guides/drip-sequence) later. Save the hook.

## The API way: your own handler

Use this when the text depends on the answers, when only some entries should get one, or when the entry should go to your CRM at the same time. Typeform posts each entry to your handler; your handler checks Typeform’s signature, picks out the number and the name, and sends the text with `POST /v1/messages`.

### Make an API key and a Typeform secret

In BlueBot, open **Developers**, then **API keys**, and make a key with the `messages:send` scope. In Typeform, add a webhook pointing at your handler’s address (for example `https://brightside.example/hooks/typeform`), then edit the webhook and set a **Secret**. Typeform signs every post with it.

Put both in your server’s environment:

```bash
export BLUEBOT_API_KEY="bb_live_…"
export TYPEFORM_SECRET="the secret you typed in Typeform"
```

### Write the handler

Typeform sends a `Typeform-Signature` header: `sha256=` and the base64 HMAC-SHA256 of the raw body, keyed with your secret. Check it before you read the body, then find the answers by their type and the question’s `ref`.

The handler sends with an Idempotency-Key built from Typeform’s token for the entry. Typeform tries a post again when your handler is slow or fails, and the same key means the same entry never texts the person twice.

```ts title="TypeScript"
// app/hooks/typeform/route.ts (Next.js). Any framework works: read the raw body first.
import { createHmac, timingSafeEqual } from 'node:crypto';
import BlueBot from '@usebluebot/sdk';

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

type Answer = { type: string; field: { ref: string }; text?: string; phone_number?: string };

function signedByTypeform(payload: string, header: string | null): boolean {
  if (!header) return false;
  const expected = 'sha256=' + createHmac('sha256', process.env.TYPEFORM_SECRET!).update(payload).digest('base64');
  return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

export async function POST(request: Request) {
  const payload = await request.text();
  if (!signedByTypeform(payload, request.headers.get('typeform-signature'))) {
    return new Response('Bad signature', { status: 401 });
  }

  const { form_response: entry } = JSON.parse(payload);
  const answers: Answer[] = entry.answers ?? [];
  const phone = answers.find((a) => a.type === 'phone_number')?.phone_number;
  const firstName = answers.find((a) => a.field.ref === 'first_name')?.text?.trim();
  if (!phone) return new Response(null, { status: 204 }); // nobody to text

  await client.messages.send(
    {
      to: phone,
      text: `Hi ${firstName ?? 'there'}, this is Dana at Brightside Remodeling. Thanks for asking for a quote. What’s a good time for a quick call this week?`,
      metadata: { source: 'typeform', form_id: entry.form_id, response_token: entry.token },
    },
    { idempotencyKey: `typeform-${entry.token}` },
  );
  return new Response(null, { status: 204 });
}
```

```python title="Python"
# Flask. Any framework works: read the raw body first.
import base64
import hashlib
import hmac
import json
import os

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

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

def signed_by_typeform(payload: bytes, header: str | None) -> bool:
    if not header:
        return False
    digest = hmac.new(os.environ["TYPEFORM_SECRET"].encode(), payload, hashlib.sha256).digest()
    expected = "sha256=" + base64.b64encode(digest).decode()
    return hmac.compare_digest(header, expected)

@app.post("/hooks/typeform")
def typeform_entry():
    payload = request.get_data()
    if not signed_by_typeform(payload, request.headers.get("Typeform-Signature")):
        abort(401)

    entry = json.loads(payload)["form_response"]
    answers = entry.get("answers", [])
    phone = next((a["phone_number"] for a in answers if a["type"] == "phone_number"), None)
    first_name = next((a.get("text", "").strip() for a in answers if a["field"]["ref"] == "first_name"), None)
    if not phone:
        return "", 204  # nobody to text

    client.messages.send(
        to=phone,
        text=f"Hi {first_name or 'there'}, this is Dana at Brightside Remodeling. "
        "Thanks for asking for a quote. What’s a good time for a quick call this week?",
        metadata={"source": "typeform", "form_id": entry["form_id"], "response_token": entry["token"]},
        idempotency_key=f"typeform-{entry['token']}",
    )
    return "", 204
```

The `ref` of a question is its name in Typeform’s data. Typeform makes one up for each question; to find yours, look at one entry’s payload in your handler’s log (without the answers), or match on the question’s type as the phone lookup does.

Underneath, the SDK call is this request:

```bash
curl -X POST "https://usebluebot.com/api/v1/messages" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: typeform-a3f9c2e1b7d84d0f" \
  -d '{
    "to": "+15555550123",
    "text": "Hi Maria, this is Dana at Brightside Remodeling. Thanks for asking for a quote. What’s a good time for a quick call this week?",
    "metadata": {
      "source": "typeform",
      "form_id": "lT4Z3j",
      "response_token": "a3f9c2e1b7d84d0f"
    }
  }'
```

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

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

const message = await client.messages.send({
  to: '+15555550123',
  text: 'Hi Maria, this is Dana at Brightside Remodeling. Thanks for asking for a quote. What’s a good time for a quick call this week?',
  metadata: {
    source: 'typeform',
    form_id: 'lT4Z3j',
    response_token: 'a3f9c2e1b7d84d0f',
  },
}, { idempotencyKey: 'typeform-a3f9c2e1b7d84d0f' });
console.log(message);
```

```python
import os
from bluebot import BlueBot

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

message = client.messages.send(
    to="+15555550123",
    text="Hi Maria, this is Dana at Brightside Remodeling. Thanks for asking for a quote. What’s a good time for a quick call this week?",
    metadata={
        "source": "typeform",
        "form_id": "lT4Z3j",
        "response_token": "a3f9c2e1b7d84d0f",
    },
    idempotency_key="typeform-a3f9c2e1b7d84d0f",
)
print(message)
```

### Jotform instead

Jotform posts a form, not JSON: the answers arrive as a JSON string in a field called `rawRequest`, keyed by each field’s name (`q3_phone`, `q4_name`). Jotform doesn’t sign its posts, so put a long random value in the address you give it (`https://brightside.example/hooks/jotform?key=…`) and refuse any post without it. Use Jotform’s `submissionID` in the Idempotency-Key (`jotform-<submissionID>`).

## Test it

Fill in your own form with your own mobile number, the way a customer would. Then look at the answer, in **Messages** in the dashboard or with `GET /v1/messages?direction=out&limit=1`.

| You got                                                                          | It means                                                                                                                      | Do next                                                                                                      |
| -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| A text on your phone from your own number within a minute                        | It works.                                                                                                                     | Turn it on for real: share the form.                                                                         |
| The text in **Messages** with `state: "queued"` and `hold.reason: "quiet_hours"` | It’s quiet hours where you are. The text waits and goes at the end of quiet hours.                                            | Test during the day, or look at **Settings**, then **Sending rules**.                                        |
| `hold.reason: "device_offline"`                                                  | Your Mac hasn’t checked in for two minutes: asleep, off or offline.                                                           | Wake it. The text goes as soon as it checks in.                                                              |
| `state: "failed"` with `failure.reason: "messages_refused"`                      | Messages on your Mac wouldn’t send it, often because a regular text needs your iPhone with Text Message Forwarding turned on. | Check the Mac app’s checks, then [Services and SMS fallback](https://usebluebot.com/docs/concepts/services).                       |
| No post in the hook’s test panel                                                 | The form never posted to the hook.                                                                                            | Check the webhook is turned on and the address is exact. Typeform’s **View deliveries** shows what it sent.  |
| A post in the test panel but no text                                             | The hook couldn’t read a phone number from the post.                                                                          | Check **Recipient** points at the phone answer.                                                              |
| `401` from your handler in Typeform’s deliveries                                 | The signature check failed.                                                                                                   | Make sure `TYPEFORM_SECRET` matches the webhook’s secret, and that you check the raw body before parsing it. |

## When it goes wrong

- **More than 50 new people in a day.** Your sending rules allow 50 people you’ve never texted per day by default. The 51st entry’s text waits with `hold.reason: "daily_new_people_limit"` until midnight. Raise the number in **Settings**, then **Sending rules**, slowly: texting many strangers at once is what gets an Apple account flagged.
- **The person typed a number that can’t get texts.** A landline or a mistyped number fails with a `failure.reason`, and the hook’s page and **Messages** show it. Nothing else happens; call them instead.
- **The person replies STOP.** BlueBot puts them on your stop list and never texts them again, from this hook or anything else. You don’t need to handle it.
- **The entry is in Spanish, or for the wrong service.** A hook sends the same template to everyone. Use the API way when the text should change with the answers.
- **Your Mac was asleep overnight.** Texts wait while the Mac is away and go when it’s back, unless they expire first (24 hours after they were due, by default). A quote request from 2 AM goes at 8 AM, after quiet hours.

## Next

- [Follow up until they answer](https://usebluebot.com/docs/guides/drip-sequence): Send two more texts over a week and stop when they reply.
- [Text Hooks](https://usebluebot.com/docs/text-hooks): Every setting on a hook, the presets and the test panel.
- [Typeform recipes](https://usebluebot.com/docs/recipes/typeform): Typeform through a Text Hook or Zapier, field by field.
- [Jotform recipes](https://usebluebot.com/docs/recipes/jotform): Jotform through a Text Hook or Zapier.
- [Holds](https://usebluebot.com/docs/concepts/holds): Why a text waits instead of failing, and when it goes.
- [Idempotency](https://usebluebot.com/docs/concepts/idempotency): How one key keeps a retried post from texting twice.
