Connect FlutterFlow to Vonage (formerly Nexmo) using a FlutterFlow API Call group pointed at a Firebase Cloud Function proxy. Because the API Secret and app private key can't ship inside a compiled Flutter app, your Cloud Function holds the credentials and exposes a simple send endpoint. FlutterFlow then POSTs to that endpoint to deliver SMS via the Vonage SMS API or omnichannel messages via the JWT-signed Messages API.
| Fact | Value |
|---|---|
| Tool | Vonage (Nexmo) API |
| Category | Communication |
| Method | FlutterFlow API Call |
| Difficulty | Intermediate |
| Time required | 45 minutes |
| Last updated | July 2026 |
Sending SMS and Omnichannel Messages from FlutterFlow with Vonage
Vonage (the company that acquired Nexmo in 2016) gives FlutterFlow developers two distinct paths. The Vonage SMS API at `rest.nexmo.com/sms/json` uses your API Key plus API Secret as query parameters — it's the quickest way to send a text from your app. The unified Messages API at `api.nexmo.com/v1/messages` adds WhatsApp, Viber, and Facebook Messenger under one endpoint, but it authenticates with a JWT signed using an application private key. Both credentials are full sending credentials: anyone who extracts them from your compiled Flutter binary could send thousands of messages billed to your account.
FlutterFlow compiles to a native Flutter client that runs on a user's device. Any value placed in an API Call header, variable, or Dart custom action ships inside the app package. This is fundamentally different from tools like Retool (which runs server-side) or Lovable (which uses Edge Functions by default). For Vonage, the correct architecture is a Firebase Cloud Function or Supabase Edge Function that stores the API Secret (or signs the JWT) and exposes a thin `/send-sms` or `/send-message` endpoint. FlutterFlow's API Call simply POSTs `{to, message, channel}` to that function and reads back a status response.
On the Vonage pricing side, both APIs are pay-as-you-go with no monthly minimum — you pay per message or per call minute. Trial accounts come with free credit but are limited to whitelisted destination numbers. The Vonage dashboard (dashboard.nexmo.com, now branded as the Vonage API Dashboard) is where you get your API Key and Secret, create a Vonage Application, download the private key, and manage purchased numbers. Keep that dashboard open alongside FlutterFlow as you work through the steps below.
Integration method
FlutterFlow uses its API Calls panel to point at a Firebase Cloud Function (or Supabase Edge Function) that holds your Vonage API Secret and signs JWTs for the Messages API. The Cloud Function exposes a simple JSON endpoint — FlutterFlow never sees the raw credential. For basic SMS you can proxy the Vonage SMS REST API; for WhatsApp and Viber you use the JWT-signed Messages API through the same Cloud Function pattern.
Prerequisites
- A Vonage account — sign up at dashboard.nexmo.com (free trial credit included)
- Your Vonage API Key and API Secret (SMS API), or a Vonage Application ID + downloaded private key (Messages API / WhatsApp)
- A Firebase project with Cloud Functions enabled (Blaze pay-as-you-go plan required for outbound network calls)
- A FlutterFlow project on a paid plan (API Calls require a paid plan in FlutterFlow)
- Basic familiarity with FlutterFlow's Action Flow Editor and App State variables
Step-by-step guide
Get your Vonage credentials and choose your API path
Open the Vonage API Dashboard at dashboard.nexmo.com and navigate to the top-right account name to reveal your API Key and API Secret. For simple SMS you only need these two values. If you want WhatsApp, Viber, or other channels, go to the 'Your Applications' section in the left sidebar, click 'Create a new application', give it a name, enable the 'Messages' capability, and click 'Generate public and private key' — your browser will download a `private.key` file immediately (save it — you cannot re-download it). Copy the Application ID shown on the confirmation screen. Decide which path you need before writing any Cloud Function code. The SMS API (`rest.nexmo.com/sms/json`) sends a POST with `api_key`, `api_secret`, `from`, `to`, and `text` as query or form parameters — it's the fastest path and takes about ten minutes end-to-end. The Messages API (`api.nexmo.com/v1/messages`) handles omnichannel but requires a JWT signed from the Application ID and the `private.key` file; plan an extra 15–20 minutes for the signing step in your Cloud Function. Also note the original Nexmo branding is now Vonage — the API endpoints still use `nexmo.com` domains but the dashboard, SDKs, and docs all say Vonage. If you see tutorials referencing Nexmo, they still apply directly.
Pro tip: Store the private.key file content securely in Firebase Secret Manager (firebase functions:secrets:set VONAGE_PRIVATE_KEY) rather than pasting it as a plain environment variable — the key is multiline and Secret Manager handles it cleanly.
Expected result: You have your API Key, API Secret, and (if using Messages API) an Application ID plus private.key file saved locally. You know which API path — SMS or Messages — you'll be building.
Deploy a Firebase Cloud Function as the secure proxy
This is the critical security step. Because the Vonage API Secret and application private key are full sending credentials, they must never appear in FlutterFlow's API Calls panel or in any Dart file — both ship inside the compiled app binary. A Firebase Cloud Function runs on Google's servers, not on the user's device, so you can safely store secrets there using Firebase environment configuration or Secret Manager. In the Firebase console, open your project and go to Functions → Get started (you need the Blaze pay-as-you-go plan for outbound HTTP). In your Cloud Functions codebase (index.js), install the Vonage Node.js SDK: `@vonage/server-sdk`. Write an HTTPS-callable function named `sendSms` (or `sendMessage` for the Messages API) that: 1. Accepts a JSON body with `{to, text}` (add a `channel` field if you want multi-channel). 2. Initialises the Vonage SDK with the API Key and API Secret from `process.env` (set via `firebase functions:config:set` or Secret Manager). 3. Calls `vonage.sms.send(...)` or the Messages API client. 4. Returns `{status, messageId}` in the response body. Deploy with `firebase deploy --only functions`. After deployment the console shows your function's HTTPS trigger URL — copy it. This URL is what FlutterFlow will call; it's safe to embed in the app because it's your own endpoint with no inherent credential.
1// Firebase Cloud Function (Node.js 18+)2const { onRequest } = require('firebase-functions/v2/https');3const { setGlobalOptions } = require('firebase-functions/v2');4const { Vonage } = require('@vonage/server-sdk');56setGlobalOptions({ region: 'us-central1' });78// Set secrets: firebase functions:secrets:set VONAGE_API_KEY VONAGE_API_SECRET9exports.sendSms = onRequest(10 { secrets: ['VONAGE_API_KEY', 'VONAGE_API_SECRET'] },11 async (req, res) => {12 res.set('Access-Control-Allow-Origin', '*');13 if (req.method === 'OPTIONS') {14 res.set('Access-Control-Allow-Methods', 'POST');15 res.set('Access-Control-Allow-Headers', 'Content-Type');16 return res.status(204).send('');17 }1819 const { to, text, from } = req.body;20 if (!to || !text) {21 return res.status(400).json({ error: 'Missing to or text' });22 }2324 const vonage = new Vonage({25 apiKey: process.env.VONAGE_API_KEY,26 apiSecret: process.env.VONAGE_API_SECRET,27 });2829 try {30 const result = await vonage.sms.send({31 to,32 from: from || 'FlutterApp',33 text,34 });35 const msg = result.messages[0];36 if (msg.status === '0') {37 return res.json({ success: true, messageId: msg['message-id'] });38 } else {39 return res.status(422).json({ success: false, errorText: msg['error-text'] });40 }41 } catch (err) {42 return res.status(500).json({ error: err.message });43 }44 }45);Pro tip: Add CORS headers (shown above) even if you only target mobile — future web builds need them, and they cost nothing to include now.
Expected result: Firebase console shows the `sendSms` function as active with an HTTPS trigger URL. Calling the URL with a test POST `{"to":"+15551234567","text":"hello"}` returns `{"success":true,"messageId":"..."}`.
Create the API Call group in FlutterFlow
Now wire up FlutterFlow to call your Cloud Function. In the FlutterFlow editor, click API Calls in the left navigation sidebar (the plug icon). Click + Add at the top and select Create API Group. Name it `VonageProxy`. In the Base URL field paste your Firebase Cloud Function HTTPS trigger URL — for example `https://us-central1-myproject.cloudfunctions.net`. Do NOT include the function name yet; that goes in the individual API Call. With the group created, click + Add again inside it and choose Create API Call. Name it `SendSms`. Set the Method to POST. In the Path field type `/sendSms`. Open the Body tab, set the Body Type to JSON, and enter the body template: ```json {"to": "[toNumber]", "text": "[messageText]"} ``` Now open the Variables tab. Create two variables: `toNumber` (type String, no default) and `messageText` (type String, no default). In the body, replace the hard-coded values with the variable syntax using double brackets: `[toNumber]` and `[messageText]`. Click the Response & Test tab. In the Test Values section enter a real phone number you own and a test message, then click Test API Call. If the Cloud Function proxy is running correctly, you should see a 200 response with `{"success":true,"messageId":"..."}`. Click 'Generate JSON Paths' on the successful response to create JSON path shortcuts: `$.success` (Boolean) and `$.messageId` (String). These are what you'll bind in the Action Flow Editor when wiring up your send button. For the Messages API / WhatsApp channel, add a second API Call in the same group called `SendWhatsApp` with path `/sendMessage` and body `{"to":"[to]","text":"[text]","channel":"whatsapp"}` — update your Cloud Function accordingly.
1{2 "Group": "VonageProxy",3 "Base URL": "https://us-central1-YOUR_PROJECT.cloudfunctions.net",4 "Calls": [5 {6 "name": "SendSms",7 "method": "POST",8 "path": "/sendSms",9 "body_type": "JSON",10 "body": {11 "to": "[toNumber]",12 "text": "[messageText]"13 },14 "variables": [15 { "name": "toNumber", "type": "String" },16 { "name": "messageText", "type": "String" }17 ],18 "json_paths": [19 { "name": "success", "path": "$.success" },20 { "name": "messageId", "path": "$.messageId" }21 ]22 }23 ]24}Pro tip: Test the API Call from inside FlutterFlow before building the UI — it saves time versus debugging a wired-up button later.
Expected result: The VonageProxy API group shows a green checkmark on the SendSms call after a successful test. You can see the parsed JSON paths for success and messageId in the Response tab.
Build the send form and wire up the Action Flow
With the API Call ready, build a simple compose screen in FlutterFlow. Add a Column widget to a new Page (name it 'SendMessage'). Inside the column add two TextField widgets — one bound to an App State String variable called `recipientNumber` (initialize to empty string) and one bound to `messageText`. Add a Button widget below and label it 'Send SMS'. Select the Button, open the Actions panel on the right, and click + Add Action. Scroll to the Backend/Database section and choose API Call. Select the VonageProxy group and the SendSms call. In the variable bindings, map `toNumber` to the App State `recipientNumber` and `messageText` to the App State `messageText` variable. Add a second action chained After first action completes → Conditional action on the response. Use the `$.success` JSON path result as the condition. If true: show a SnackBar with the text 'Message sent!' and clear the text fields (set App State `messageText` to ''). If false: show a SnackBar with an error message by binding the response body or a static 'Send failed, check the number.' message. For the recipient number field, use a PhoneNumberTextField widget if available (or a plain TextField with keyboard type set to Phone). Remind users to include the country code with `+` prefix (e.g. `+14155552671`) — Vonage requires E.164 format and rejects numbers without the leading `+`. If you want to show a loading indicator while the API Call is in flight, add a Conditional widget that checks a Boolean App State variable `isSending` and overlay a CircularProgressIndicator. Set `isSending` to true at the start of the action chain and back to false after the SnackBar.
Pro tip: The Vonage SMS response `status` field is a string '0' for success, NOT an HTTP 200 code — your Cloud Function already handles this, but if you ever call Vonage directly in testing, remember that a 200 HTTP response can still contain a non-zero status string indicating failure.
Expected result: Tapping Send SMS on the FlutterFlow app triggers the Cloud Function, the Vonage API sends the message, and the user sees a 'Message sent!' SnackBar. The messageId appears in the Cloud Function logs confirming delivery queued.
Handle delivery receipts and inbound SMS via a Cloud Function webhook
Vonage calls your webhook URL whenever a message is delivered (or fails) and whenever an inbound SMS arrives on your Vonage number. FlutterFlow cannot receive webhooks directly because it's a client app running on a device — there's no persistent public URL. The solution is another Firebase Cloud Function that acts as the webhook receiver. In the Vonage API Dashboard, go to your account settings and find the 'Default SMS Settings' section. Paste your Cloud Function URL into the Delivery Receipt URL field (e.g. `https://us-central1-myproject.cloudfunctions.net/vonageDlr`). For inbound SMS, set the Inbound SMS URL field to another function URL (e.g. `/vonageInbound`). Write a second Cloud Function `vonageDlr` that receives a POST from Vonage with fields `messageId`, `status`, `to`, and `timestamp`. Write these to a Firestore collection `delivery_receipts` keyed by `messageId`. Then in FlutterFlow, when you call SendSms and get back a `messageId`, store it in a Firestore document for that session. On the confirmation screen, use a Firestore stream widget filtered on that `messageId` — when Vonage fires the DLR and your Cloud Function writes to Firestore, the Flutter UI updates automatically in real time without polling. For inbound SMS (replies), write a third Cloud Function `vonageInbound` that receives `from`, `to`, `text`, and `messageId` and writes them into a Firestore `messages` collection. Your FlutterFlow chat or inbox screen can then query this collection and show replies. Note: If you're on a Vonage Messages API flow (WhatsApp etc.), delivery webhook configuration is on the Vonage Application settings page under 'Status URL' and 'Inbound URL' rather than the default SMS settings.
1// Cloud Function: receive Vonage DLR webhook2exports.vonageDlr = onRequest(async (req, res) => {3 const { messageId, status, to, timestamp } = req.body;4 if (messageId) {5 await admin.firestore().collection('delivery_receipts').doc(messageId).set({6 status,7 to,8 timestamp: timestamp || Date.now(),9 updatedAt: admin.firestore.FieldValue.serverTimestamp(),10 });11 }12 // Vonage expects 200 OK quickly13 res.sendStatus(200);14});Pro tip: Return 200 to Vonage immediately — if your function takes more than a few seconds to respond, Vonage may retry the webhook and create duplicate Firestore writes. Use async writes with no await if processing is slow.
Expected result: The Vonage dashboard shows your DLR and inbound URLs saved. When you send a test SMS, within seconds the delivery_receipts Firestore collection gains a document with status 'delivered'. Your FlutterFlow confirmation screen can now stream this status in real time.
Test the full flow and handle common errors
Send a real test SMS from your FlutterFlow app (in Run mode on a device or via the FlutterFlow Test Mode APK). Watch the Cloud Function logs in the Firebase console (Functions → Logs) for the raw Vonage response. Check that `messages[0].status === '0'` in the response — a status of '0' means queued for delivery, not that it's been delivered to the handset yet (delivery confirmation comes from the webhook). A non-zero status string like '6' means the number is unreachable and '7' means a blacklisted number. For the Messages API (WhatsApp) path, make sure the recipient has opted in to receive WhatsApp messages from your Vonage number — WhatsApp enforces user consent strictly and will reject messages to users who haven't started a conversation or received a template message approval. Check the Vonage dashboard under 'Messages & Dispatch' to see sent message status and WhatsApp template approval status. For web builds of your FlutterFlow app, CORS applies: the Cloud Function's `Access-Control-Allow-Origin: *` header (shown in the code above) handles preflight requests. If you see `XMLHttpRequest error` on web, open your browser's developer tools, check the Network tab, and confirm the preflight OPTIONS request is returning 204 with the correct CORS headers from your Cloud Function. If you plan to scale beyond casual use, note Vonage's default SMS throughput is approximately 1 message per second per long code number. For higher volumes, contact Vonage to upgrade your throughput or use a short code. Check current limits in the Vonage developer documentation since these caps change with account tier.
Pro tip: If you'd rather skip the Cloud Function setup entirely, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.
Expected result: End-to-end: entering a phone number and message in FlutterFlow, tapping Send, and receiving the SMS on the target device within a few seconds. Cloud Function logs confirm the Vonage API was called with status 0. No credentials are visible anywhere in the FlutterFlow project.
Common use cases
Appointment reminder app that sends SMS confirmations
A FlutterFlow scheduling app stores bookings in Firestore. When a booking is confirmed, a Cloud Function triggers a Vonage SMS via the SMS API to the patient's phone number. A FlutterFlow action also lets staff manually trigger a reminder from the admin panel.
Build an appointments screen in FlutterFlow where tapping 'Send Reminder' calls our /send-sms Cloud Function with the patient's number and appointment time, then shows a SnackBar confirming the SMS was queued.
Copy this prompt to try it in FlutterFlow
E-commerce order status app with WhatsApp notifications
A FlutterFlow storefront sends order confirmations and shipping updates over WhatsApp using the Vonage Messages API JWT flow. Customers see a WhatsApp-style chat widget in the app listing order history, while the Cloud Function proxies outbound messages and a webhook writes inbound replies back to Firestore.
Add a WhatsApp notification flow to my FlutterFlow shop: when an order ships, our Cloud Function sends a templated WhatsApp message via Vonage Messages API and the customer can reply to get support.
Copy this prompt to try it in FlutterFlow
OTP / two-factor authentication flow
A FlutterFlow app uses the Vonage SMS API (or Vonage Verify) to dispatch a one-time passcode during account sign-up or sensitive actions like fund transfers. The Cloud Function generates the OTP, stores a hash in Firestore with an expiry, sends the SMS, and exposes a separate `/verify-otp` endpoint that FlutterFlow calls on code entry.
Implement phone-number verification in my FlutterFlow app: trigger an SMS OTP via our Cloud Function, show a 6-digit entry field in FlutterFlow, and verify the code before unlocking the next screen.
Copy this prompt to try it in FlutterFlow
Troubleshooting
SMS API returns status '6' (Invalid To) or status '7' (Number Blacklisted) instead of '0'
Cause: The destination number is formatted incorrectly (missing + or country code), is on Vonage's blacklist, or the trial account hasn't whitelisted the number.
Solution: Ensure all numbers are in E.164 format with a leading + (e.g. +14155552671). For trial accounts, go to the Vonage dashboard → Numbers → Whitelisted Numbers and add the test number. Check the Vonage error code reference for the full status code list.
Cloud Function returns 401 or Vonage returns 'Authentication failed'
Cause: The API Key or API Secret in the Cloud Function environment doesn't match the account, or you're using trial credentials against a live endpoint.
Solution: In the Firebase console go to Functions → your function → Configuration and verify the VONAGE_API_KEY and VONAGE_API_SECRET secret values match exactly what's shown in the Vonage dashboard top-right. Regenerate the secret in the Vonage dashboard if needed and update the Firebase secret.
XMLHttpRequest error on web build — CORS blocked
Cause: The FlutterFlow web build (running in a browser) sends a preflight OPTIONS request to the Cloud Function that doesn't return Access-Control-Allow-Origin headers.
Solution: Add the CORS headers shown in the Cloud Function code above. After updating the function, redeploy with `firebase deploy --only functions`. Also confirm the FlutterFlow API Call's Base URL exactly matches the deployed Cloud Function URL (no trailing slash mismatches).
1res.set('Access-Control-Allow-Origin', '*');2if (req.method === 'OPTIONS') {3 res.set('Access-Control-Allow-Methods', 'POST');4 res.set('Access-Control-Allow-Headers', 'Content-Type');5 return res.status(204).send('');6}Messages API (WhatsApp) returns 401 — JWT authentication failed
Cause: The JWT token has expired (Messages API JWTs are short-lived, typically 15 minutes), or the Application ID in the JWT payload doesn't match the Vonage Application that owns the WhatsApp channel.
Solution: JWTs for the Messages API must be re-signed server-side for each request or at least refreshed before expiry — don't cache and reuse a token. In your Cloud Function, generate a fresh JWT on every request using the `@vonage/jwt` package. Double-check the Application ID matches the one listed in your Vonage Application settings page.
Best practices
- Never place the Vonage API Secret or application private key in a FlutterFlow API Call header, App Values, or Dart custom action — both the API Secret and private key are full sending credentials that anyone can extract from a compiled Flutter binary.
- Use Firebase Secret Manager (or Supabase Vault) instead of plain environment variables for the private key — the multiline PEM format can break with plain `functions:config:set` and Secret Manager handles it reliably.
- Generate fresh JWTs in the Cloud Function on every request to the Messages API — don't cache a token and reuse it across calls, since an expired JWT causes hard-to-diagnose 401 errors mid-session.
- Always use E.164 phone number format (e.g. +14155552671) for both `from` and `to` fields — Vonage will silently fail or return status '6' for numbers missing the leading + and country code.
- Check `messages[0].status === '0'` in your Cloud Function response logic, not just the HTTP status code — Vonage can return HTTP 200 with a non-zero status string indicating delivery failure.
- For high-volume sending (more than one message per second), contact Vonage to upgrade your throughput or purchase a short code — the default 1 msg/sec throughput per long code number is a hard rate limit.
- Configure Delivery Receipt webhooks to a Cloud Function and write status to Firestore — this gives you real-time delivery confirmation in your FlutterFlow UI without polling and works even if the app is closed.
Alternatives
Plivo is typically 30–50% cheaper per SMS and uses a simpler single HTTP-Basic authentication with no JWT signing required, making it the easier choice if you only need SMS and don't need WhatsApp or Viber.
Twilio has the largest global reach and most mature documentation ecosystem, making it the safer choice for production apps that need voice, video, and SMS in one platform despite its higher per-message cost.
Sinch bundles SMS, voice, and email under one SDK and offers strong carrier relationships in emerging markets, making it worth evaluating if your app targets Southeast Asia or Latin America.
Frequently asked questions
Do I need the Vonage Messages API or is the SMS API enough?
Use the SMS API (`rest.nexmo.com/sms/json`) if you only need to send plain text messages to regular phone numbers — it's simpler to set up with just an API Key and Secret. Use the Messages API (`api.nexmo.com/v1/messages`) if you need WhatsApp, Viber, Facebook Messenger, or MMS — it requires a Vonage Application, a private key, and JWT signing but gives you one unified endpoint for all channels.
Can I put the Vonage API Key (not the Secret) directly in the FlutterFlow API Call?
The API Key alone cannot send messages — it always needs to be paired with the API Secret or a JWT. Even if you were to split them, having the API Key visible makes it easier for attackers to brute-force or guess the secret. Use the Cloud Function proxy for both values to keep your account fully protected.
Why can't I test the Cloud Function call in FlutterFlow's web preview?
FlutterFlow web preview runs in an iframe inside the FlutterFlow editor. Browser security policies may block certain cross-origin requests from iframes differently than from a real browser tab. Use FlutterFlow's Run Mode (which opens a full browser tab) for API Call testing, or export a Test Mode APK and test on a device for the most accurate results.
Vonage shows the SMS as 'delivered' in their dashboard but the user didn't receive it — what's wrong?
'Delivered' in the Vonage dashboard means the message was accepted by the carrier, not necessarily that it appeared on the handset. Carrier-side filtering (spam filters, opt-out lists, or TCPA compliance blocks in the US) can suppress delivery silently. Check that your Vonage number is registered for the destination country, that your message content doesn't trigger spam filters, and that the recipient hasn't opted out of messages from your number.
How do I handle inbound replies from users in my FlutterFlow app?
Set the Inbound SMS Webhook URL in the Vonage dashboard to a Firebase Cloud Function URL. That function writes incoming messages to a Firestore collection. In FlutterFlow, use a Firestore real-time stream query on that collection to update a ListView automatically when new messages arrive — this is the standard pattern since FlutterFlow can't receive webhooks directly.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation