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.
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.
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 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:
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.
// 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 });
}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:
/v1/messagescurl -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"
}
}'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 . |
| 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.