# Twilio

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Twilio by building a Firebase Cloud Function that sends SMS or starts a Twilio Verify OTP check, then pointing a FlutterFlow API Call at that function URL. Twilio authenticates with HTTP Basic Auth using your Account SID and Auth Token — those credentials must never appear in a FlutterFlow API Call because they ship inside the compiled app bundle.

## Add SMS and Phone Verification to Your FlutterFlow App with Twilio

Twilio is the industry standard for transactional SMS — appointment reminders, order confirmations, OTP login codes, and security alerts. For a FlutterFlow app builder, the most valuable use-case is phone number verification during signup: using Twilio Verify to send a one-time code and confirm the user's phone before they access the app. This eliminates fake accounts and adds a second authentication factor. The cost is approximately $0.05 per verification (check current pricing in the Twilio console), and each sending phone number costs around $1.15 per month — extremely affordable for even small apps.

The architecture is straightforward but non-negotiable on one point: Twilio authenticates with HTTP Basic Auth using your Account SID as the username and your Auth Token as the password. If you configure that Basic Auth header directly in a FlutterFlow API Call, the Auth Token gets compiled into your Android APK and iOS IPA. Anyone who unpacks the app can extract it. The correct pattern is a Firebase Cloud Function that stores the SID and Auth Token as environment variables and is the only component that ever calls Twilio.

For raw SMS sends (not OTP), the Twilio Messages endpoint also requires form-urlencoded body params — not JSON. The fields are `To`, `From`, and `Body`, where `To` must be an E.164 number (starting with `+` and country code). Getting the format wrong returns error 21211. Twilio trial accounts add the text 'Sent from your Twilio trial account' to every message and can only send to verified numbers — upgrade to a paid account for production sends.

## Before you start

- A Twilio account (free trial works for initial testing with verified numbers)
- A Twilio phone number for sending SMS (approximately $1.15/month in the Twilio console)
- A Firebase project on the Blaze plan (required for Cloud Functions HTTP triggers and outbound network calls)
- FlutterFlow project with Firebase configured in Settings & Integrations → Firebase
- Phone numbers stored in E.164 format (+14155551234) in your Firestore user documents

## Step-by-step guide

### 1. Set up Twilio and gather your credentials

Log in to your Twilio console at console.twilio.com. On the dashboard home page you'll see your Account SID (starts with AC) and Auth Token (click the eye icon to reveal it). Copy both — you'll store them as Cloud Function environment variables, never in FlutterFlow. If you're implementing OTP verification, click on Verify under the Products menu, then create a new Verify Service. Give it a friendly name (e.g., 'MyApp Verify') and copy the Service SID (starts with VA). If you're sending raw SMS, go to Phone Numbers → Manage → Active Numbers to find your Twilio sending number in E.164 format. For trial accounts, go to Phone Numbers → Verified Caller IDs and add each phone number you want to test with — trial accounts can only send to verified numbers. Make a note of which Twilio region you're using; the API base URL is the same for all regions (`https://api.twilio.com/2010-04-01`) but this matters if you later look at the console logs.

**Expected result:** You have your Account SID, Auth Token, a Twilio phone number, and (for OTP) a Verify Service SID ready to use.

### 2. Deploy a Firebase Cloud Function that calls Twilio server-side

In the Firebase console for your project, navigate to Functions and click Get Started (or Create Function if you've used Functions before). Make sure your Firebase project is on the Blaze plan — Cloud Functions need the Blaze plan for outbound HTTP requests to external services like Twilio. You can write the function in the inline editor or deploy via CLI. The function below handles two cases: sending a raw SMS and starting a Twilio Verify OTP. It reads the Twilio credentials from environment configuration so they never appear in code. The function accepts a JSON body with an `action` field ('send_sms' or 'send_otp' or 'check_otp'), a `to` phone number in E.164 format, and for SMS a `body` message string. It returns a JSON response with a `success` boolean and the Twilio response details. CORS headers are included so the function can be called from a FlutterFlow web build. Set your credentials with the Firebase CLI commands shown in the tip field, then deploy. The deployed function URL will look like `https://us-central1-your-project-id.cloudfunctions.net/twilioProxy`.

```
// Firebase Cloud Function: functions/index.js
const functions = require('firebase-functions');
const https = require('https');
const querystring = require('querystring');

exports.twilioProxy = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
  res.set('Access-Control-Allow-Headers', 'Content-Type');

  if (req.method === 'OPTIONS') {
    return res.status(204).send('');
  }

  const ACCOUNT_SID = functions.config().twilio.account_sid;
  const AUTH_TOKEN = functions.config().twilio.auth_token;
  const FROM_NUMBER = functions.config().twilio.from_number;
  const VERIFY_SID = functions.config().twilio.verify_sid;

  const { action, to, body, code } = req.body;

  // Validate E.164 format
  if (!to || !/^\+[1-9]\d{7,14}$/.test(to)) {
    return res.status(400).json({ success: false, error: 'Invalid phone number. Use E.164 format (+14155551234).' });
  }

  const authHeader = 'Basic ' + Buffer.from(ACCOUNT_SID + ':' + AUTH_TOKEN).toString('base64');

  if (action === 'send_sms') {
    const params = querystring.stringify({ To: to, From: FROM_NUMBER, Body: body || 'Hello from the app' });
    const options = {
      hostname: 'api.twilio.com',
      path: `/2010-04-01/Accounts/${ACCOUNT_SID}/Messages.json`,
      method: 'POST',
      headers: {
        'Authorization': authHeader,
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(params)
      }
    };
    return makeRequest(options, params, res);
  }

  if (action === 'send_otp') {
    const params = querystring.stringify({ To: to, Channel: 'sms' });
    const options = {
      hostname: 'verify.twilio.com',
      path: `/v2/Services/${VERIFY_SID}/Verifications`,
      method: 'POST',
      headers: {
        'Authorization': authHeader,
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(params)
      }
    };
    return makeRequest(options, params, res);
  }

  if (action === 'check_otp') {
    const params = querystring.stringify({ To: to, Code: code });
    const options = {
      hostname: 'verify.twilio.com',
      path: `/v2/Services/${VERIFY_SID}/VerificationChecks`,
      method: 'POST',
      headers: {
        'Authorization': authHeader,
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(params)
      }
    };
    return makeRequest(options, params, res);
  }

  return res.status(400).json({ success: false, error: 'Invalid action' });
});

function makeRequest(options, postData, res) {
  return new Promise((resolve) => {
    const request = https.request(options, (response) => {
      let data = '';
      response.on('data', (chunk) => data += chunk);
      response.on('end', () => {
        try {
          const parsed = JSON.parse(data);
          resolve(res.status(200).json({ success: true, data: parsed }));
        } catch (e) {
          resolve(res.status(200).json({ success: true, raw: data }));
        }
      });
    });
    request.on('error', (e) => resolve(res.status(500).json({ success: false, error: e.message })));
    if (postData) request.write(postData);
    request.end();
  });
}
```

**Expected result:** The Cloud Function deploys successfully and its URL appears in the Firebase console under Functions. Testing it with a POST to the function URL with action=send_otp sends an SMS to your test phone.

### 3. Add the Cloud Function API Group in FlutterFlow

In your FlutterFlow project, click API Calls in the left navigation panel. Click + Add and choose Create API Group. Name it 'TwilioProxy'. Set the Base URL to your Cloud Function's base URL — for example, `https://us-central1-your-project-id.cloudfunctions.net`. Leave Authentication as None (the function's security is handled server-side). Now add the API calls inside this group. Add the first call: click + Add API Call, name it 'SendOTP'. Set the method to POST and the API URL to `/twilioProxy`. In the Body tab, set the body type to JSON. Add the fields: `action` with a fixed value of 'send_otp', and a variable `phone` of type String bound to the `to` key. Add a second API Call named 'CheckOTP': same URL, same JSON body type, with fields `action` = 'check_otp', variable `phone` bound to `to`, and variable `code` (String) bound to `code`. Go to the Response & Test tab for SendOTP. Enter a test phone number (your own in E.164 format) and click Test API Call — you should receive an SMS. In the JSON Path field, add a path `$.success` to extract the result. You can use this path in conditional actions to determine whether to show an error or advance the user to the code entry screen.

**Expected result:** The SendOTP API Call test returns a 200 response with success: true and sends an SMS to the test number. The CheckOTP call structure is in place and ready to wire.

### 4. Wire the OTP flow to your FlutterFlow signup screen

Open the signup page in FlutterFlow's canvas. You'll need a phone number TextField, a 'Send Code' button, a 6-digit code TextField (initially hidden), and a 'Verify' button (initially hidden). Use the Conditional Visibility settings on the code TextField and Verify button to show them only after the OTP is sent — you can control this with a Page State boolean variable named `otpSent`. First, add a Custom Function to validate the phone number before calling the API. Click Custom Code in the left nav, then + Add → Function. Name it 'validateE164'. It takes a String argument and returns a Boolean: `return RegExp(r'^\+[1-9]\d{7,14}$').hasMatch(phone);`. Now wire the 'Send Code' button. In its Actions panel: first call the validateE164 Custom Function — if it returns false, show a Snackbar 'Please enter a valid phone number with country code (e.g. +14155551234)'. If it returns true, call the SendOTP Backend/API Call with the phone field value. On success (check `$.success` is true), set the `otpSent` Page State to true (which reveals the code entry UI). Wire the 'Verify' button to call CheckOTP with the phone and code values. On success where the response `$.data.status` equals 'approved', navigate to the app's main screen and store the verified phone in the user's Firestore profile.

```
// FlutterFlow Custom Function: validateE164
// Return type: Boolean, argument: phone (String)
bool validateE164(String phone) {
  return RegExp(r'^\+[1-9]\d{7,14}$').hasMatch(phone);
}
```

**Expected result:** The signup flow sends an OTP SMS, reveals the code entry field, and navigates to the main app screen when the correct code is entered. Invalid numbers show a validation error before calling the API.

### 5. Test end-to-end on a real device and handle edge cases

The OTP and SMS flow uses plain HTTPS API calls (to the Cloud Function), so it works in FlutterFlow's web Run Mode for initial testing. However, to fully test the user experience — keyboard type, autofill behavior on iOS and Android, and loading states — test on a real device using FlutterFlow's 'Test on Device' option. Pay attention to these edge cases: If the user taps 'Send Code' twice, Twilio Verify handles this gracefully (it cancels the first pending verification and starts a new one), but you should disable the button during the API Call to prevent accidental double-taps. Add a Page State boolean `isLoading` and use it to show a CircularProgressIndicator and disable the button while the API Call is in flight. For resend functionality, add a countdown timer (30 or 60 seconds) before the Resend button becomes active — this prevents abuse. If the Verify step fails with `$.data.status` = 'pending' or 'expired', show a clear error message and let the user request a new code. Trial account limitation: Twilio trial accounts prepend 'Sent from your Twilio trial account' to every SMS and can only send to phone numbers in your Verified Caller IDs list. This is normal during development; it disappears once you upgrade to a paid account.

**Expected result:** The complete OTP flow works on a real iOS or Android device: sends an SMS, accepts the code, verifies it against Twilio, and navigates to the main app on success.

## Best practices

- Always route Twilio calls through a Firebase Cloud Function — never put the Account SID or Auth Token in a FlutterFlow API Call header or App Values constant, as they will be compiled into the app bundle.
- Use Twilio Verify for OTP flows rather than generating and storing raw codes yourself — Twilio handles code generation, delivery, expiration, and attempt counting, which is more secure and LESS code.
- Validate phone numbers in E.164 format (+14155551234) in FlutterFlow before calling the Cloud Function to catch format errors early and avoid Twilio's 21211 error.
- Disable the Send Code button during the API Call and add a 30-60 second cooldown before the Resend button activates — this prevents code-bombing and accidental double-sends.
- Store verified phone numbers in the user's Firestore profile after OTP confirmation — never store unverified numbers, and never send transactional SMS to a number that has not been verified.
- For Twilio Messaging Services (Sender Pool), use the Messaging Service SID instead of a specific From number when your volume grows — this improves deliverability and allows number pooling.
- Check the Twilio console's Error Logs regularly during development — error 30034 (message body too long), 30008 (unknown error), and 21614 (not a mobile number) all have specific fixes documented in the Twilio error dictionary.

## Use cases

### On-demand service app with SMS OTP phone verification on signup

When a new user enters their phone number during registration, the FlutterFlow app calls a Cloud Function that triggers Twilio Verify to send a 6-digit OTP. The user enters the code in a FlutterFlow text field, and a second API Call to a verification-check function confirms the code. Approved users proceed to the main app; rejected codes show an error.

Prompt example:

```
On the signup page, add a phone number text field, a 'Send Code' button that calls the sendVerification Cloud Function, a 6-digit code input field, and a 'Verify' button that calls the checkVerification function. Show a success toast and navigate to the home page on success.
```

### Booking app that sends SMS appointment reminders

When a user books an appointment and a Firestore document is created, a Cloud Function triggers (or a scheduled function runs 24 hours before) and sends an SMS reminder via Twilio with the appointment date, time, and location. The user's phone number is stored in their Firestore profile in E.164 format.

Prompt example:

```
When the booking Firestore document status changes to 'confirmed', call the Cloud Function sendSmsReminder with the user's phone number and appointment details.
```

### E-commerce app with order status SMS notifications

When an order's status updates in Firestore (shipped, delivered, exception), a Cloud Function sends a Twilio SMS to the customer's verified phone number with the new status and a tracking link. The FlutterFlow app triggers these status updates; the SMS goes server-side.

Prompt example:

```
Add an 'Update Status' dropdown to the admin order page. When the status changes, call the Cloud Function sendOrderSms with the customer's phone number and the new status string.
```

## Troubleshooting

### Cloud Function returns 400 with 'Invalid phone number' even though the number looks correct

Cause: The phone number is not in E.164 format — it's missing the leading + and country code, or contains spaces, dashes, or parentheses.

Solution: Strip all formatting from the number on the client side using a FlutterFlow Custom Function before sending. E.164 requires + followed by 7-15 digits with no spaces or punctuation: +14155551234, not (415) 555-1234 or 14155551234.

### Twilio returns HTTP 400 with error code 21211 'To' number is not a valid phone number

Cause: The number passed to Twilio's Messages endpoint is not a valid E.164 phone number, or it starts with + when the to field expects a specific format.

Solution: Confirm you're passing a full E.164 number including country code. Double-check that the phone field in FlutterFlow is bound to the actual text field value and not an empty string or placeholder text.

### OTP check always returns status 'pending' or 'expired' even for the correct code

Cause: The Verify code has expired (codes expire after 10 minutes), or the user entered the code with a leading/trailing space.

Solution: Add a .trim() call in the Custom Function or Cloud Function before passing the code to Twilio Verify. If the code is expired, prompt the user to request a new one. Verify codes expire in 10 minutes by default — you can adjust this in the Verify Service settings in the Twilio console.

### Cloud Function call fails with a CORS error in FlutterFlow's web preview

Cause: The Cloud Function is not returning the Access-Control-Allow-Origin header, or the OPTIONS preflight is not being handled.

Solution: Verify the Cloud Function code includes the CORS headers shown in Step 2 and returns 204 for OPTIONS requests. Also confirm you are calling the deployed function URL (https://us-central1-...) and not a localhost URL, which would never be reachable from the web preview.

## Frequently asked questions

### Can I call Twilio directly from a FlutterFlow API Call without a Cloud Function?

Technically yes for web builds if you skip Basic Auth and use a different auth method — but for any production app, no. The Auth Token in a client API Call is compiled into the app bundle and can be extracted by anyone who unpacks your APK or IPA. Even on web, the network tab exposes it. Always use a Cloud Function proxy for Twilio.

### Does this work in FlutterFlow's web Run/Test Mode or only on a real device?

The Cloud Function API Call works in FlutterFlow's web Run Mode because it's a plain HTTPS call with no native device plugins. You can test the full OTP send-and-verify flow in the browser preview. For the complete real-device experience including keyboard autofill behavior, test with FlutterFlow's Test on Device option.

### How much does Twilio Verify cost compared to raw SMS?

Twilio Verify charges approximately $0.05 per verification attempt (check current pricing in the Twilio console), which includes the SMS delivery cost. Raw SMS via the Messages endpoint charges separately for the SMS (~$0.0079 per message in the US) and your Twilio phone number (~$1.15/month). For OTP flows, Verify is usually more economical and far safer since you don't manage raw codes.

### My Twilio trial account keeps adding 'Sent from your Twilio trial account' to every SMS — how do I remove it?

This prefix is added by Twilio to all messages from trial accounts. It disappears automatically when you upgrade to a paid account and add a credit card in the Twilio console. Trial accounts also restrict sending to verified numbers only — upgrade removes both limitations.

### Can I use Twilio to send WhatsApp messages from FlutterFlow?

Yes, Twilio resells WhatsApp messaging through its API using the same Messages endpoint but with a `whatsapp:+14155551234` format for the To and From fields. However, this page covers the native Meta WhatsApp Cloud API — see the WhatsApp Business API integration page for the direct Meta approach, which avoids Twilio's markup on WhatsApp messages.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/twilio-c8fd2
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/twilio-c8fd2
