Connect FlutterFlow to Zendesk Sunshine Conversations (formerly Smooch) via a Firebase Cloud Function that mints short-lived JWTs from your App Key Secret — the secret never enters the compiled app. FlutterFlow calls your API Group to send messages, while inbound replies flow from Sunshine webhooks into Firestore, which FlutterFlow streams in real-time for a live chat UI.
| Fact | Value |
|---|---|
| Tool | Zendesk Sunshine Conversations (formerly Smooch) |
| Category | Communication |
| Method | FlutterFlow API Call |
| Difficulty | Advanced |
| Time required | 90 minutes |
| Last updated | July 2026 |
Omnichannel Messaging in FlutterFlow with Sunshine Conversations
Zendesk Sunshine Conversations — launched as Smooch and rebranded after Zendesk's acquisition — is an omnichannel messaging platform that lets you connect a single API to WhatsApp, SMS, Facebook Messenger, Instagram Direct, Viber, and your own in-app chat simultaneously. When a customer sends a message on any of those channels, it arrives in one unified conversation thread. You respond once via the API and Sunshine routes it back to the right channel. This makes it ideal for customer support or sales FlutterFlow apps where users might contact you from any platform.
The authentication model is JWT-based. Your Sunshine Conversations App has an App Key ID and an App Key Secret. Every API request must include a JWT signed from those credentials with an `app` scope. Because the Key Secret is a full-privilege credential — anyone who has it can read and send all conversations in your App — it cannot live inside the FlutterFlow binary. The correct architecture is a Firebase Cloud Function (or Supabase Edge Function) that holds the secret in environment variables, signs a JWT per request, and acts as the intermediary between FlutterFlow and the Sunshine API.
Inbound messages (customers replying via WhatsApp, Messenger, etc.) come in via webhooks — Sunshine sends an HTTP POST to a URL you configure. FlutterFlow cannot be a webhook target (it's a client app, not a server). The solution is a second Cloud Function endpoint that receives webhook events and writes each message to Firestore. Your FlutterFlow chat screen binds a Firestore real-time stream to the conversation, so new messages appear the moment they hit Firestore. Sunshine Conversations is a paid Zendesk add-on — pricing depends on your Zendesk Suite plan, so check the current rates at zendesk.com.
Integration method
FlutterFlow sends outbound messages by calling your Cloud Function, which signs a short-lived JWT from the Sunshine Conversations App Key Secret and forwards the request to the Sunshine Conversations v2 API. Inbound replies from customers arrive via webhook to a second Cloud Function that writes messages into Firestore — FlutterFlow binds a Firestore real-time stream to the chat ListView so new messages appear without polling. The Key Secret never touches the compiled app.
Prerequisites
- A Zendesk account with Sunshine Conversations enabled — it's part of certain Zendesk Suite plans; check pricing at zendesk.com/pricing
- Access to the Sunshine Conversations dashboard to create an App, get the App ID, Key ID, and Key Secret
- A Firebase project with Cloud Functions on the Blaze plan (required for external HTTP calls) and Firestore enabled
- A FlutterFlow project with Firestore integration configured (Settings & Integrations → Firebase → connect your Firebase project)
- Familiarity with FlutterFlow's API Calls panel, Action Flow Editor, and Firestore widget bindings
Step-by-step guide
Create a Sunshine Conversations App and retrieve credentials
Log in to your Zendesk account and navigate to the Sunshine Conversations dashboard (app.smooch.io or the Zendesk admin panel under 'Channels'). Click 'Create App' and give it a name matching your FlutterFlow project. Once created, you'll see your **App ID** in the dashboard — copy it. This is a public identifier used in every API endpoint path. Next, generate API credentials: go to the App Settings → API Keys (or 'Credentials' section) and click 'Create Key'. Give it a name like 'FlutterFlow Cloud Function'. Copy the **Key ID** and **Key Secret** immediately — the Key Secret is shown only once (treat it like a password). Store both somewhere secure like a password manager. Note your data region: Sunshine Conversations runs on two regions. US accounts use `api.smooch.io` as the base URL; EU accounts use `api.eu-1.smooch.io`. Check your Zendesk data residency settings to determine which base URL to use. Using the wrong region returns 401 or 404 errors even with correct credentials. If you plan to receive inbound messages from channels like WhatsApp or SMS, you'll also need to configure those channel connectors in the dashboard — go to App → Integrations → select the channel (e.g., WhatsApp Business) and follow the connection wizard. Channel configuration is done entirely in the Sunshine dashboard, not in FlutterFlow.
Pro tip: The App ID, Key ID, and Key Secret are three distinct things. The App ID goes in the API URL path and can be treated as semi-public. The Key ID goes in the JWT header as 'kid'. The Key Secret signs the JWT and must never leave your Cloud Function — it's the only one that grants sending access.
Expected result: You have App ID, Key ID, and Key Secret saved. You know your data region (US or EU). Your Sunshine Conversations App is active in the dashboard.
Deploy two Cloud Functions: JWT proxy and webhook relay
You need two Firebase Cloud Functions: one for outbound messages (signs a JWT per request, calls the Sunshine API) and one for inbound webhooks (receives Sunshine events, writes to Firestore). **Function 1: `sendSunshineMessage`** — accepts a POST from FlutterFlow with `{conversationId, messageText}`, signs a JWT using your Key ID and Key Secret (using the `jsonwebtoken` npm package), and calls `POST https://api.smooch.io/v2/apps/{appId}/conversations/{id}/messages` with a `{author: {type: 'business'}, content: {type: 'text', text: '...'}}` body. **Function 2: `sunshineWebhook`** — receives POST events from Sunshine (message:appUser events), extracts the conversation ID, message text, sender info, and timestamp, then writes a document to Firestore at `/conversations/{conversationId}/messages/{messageId}`. This is the Firestore document your FlutterFlow app will stream in real-time. After deploying, go to your Sunshine Conversations App dashboard → Webhooks → Create Webhook. Set the target URL to your `sunshineWebhook` function URL and check the `message:appUser` trigger (fires when a customer sends a message). Sunshine will send a POST to your function each time a customer replies. Set Firebase environment config: `firebase functions:config:set sunshine.app_id='YOUR_APP_ID' sunshine.key_id='YOUR_KEY_ID' sunshine.key_secret='YOUR_KEY_SECRET' sunshine.region='us'`
1// index.js (Firebase Cloud Functions)2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const axios = require('axios');5const jwt = require('jsonwebtoken');67admin.initializeApp();8const db = admin.firestore();910const APP_ID = functions.config().sunshine.app_id;11const KEY_ID = functions.config().sunshine.key_id;12const KEY_SECRET = functions.config().sunshine.key_secret;13const REGION = functions.config().sunshine.region || 'us';14const BASE_URL = REGION === 'eu'15 ? 'https://api.eu-1.smooch.io/v2'16 : 'https://api.smooch.io/v2';1718function makeJwt() {19 return jwt.sign({ scope: 'app' }, KEY_SECRET, {20 header: { kid: KEY_ID },21 expiresIn: '60s'22 });23}2425// Outbound: FF calls this to send a message26exports.sendSunshineMessage = functions.https.onRequest(async (req, res) => {27 res.set('Access-Control-Allow-Origin', '*');28 if (req.method === 'OPTIONS') {29 res.set('Access-Control-Allow-Methods', 'POST');30 res.set('Access-Control-Allow-Headers', 'Content-Type');31 return res.status(204).send('');32 }33 const { conversationId, messageText } = req.body;34 if (!conversationId || !messageText) {35 return res.status(400).json({ error: 'conversationId and messageText required' });36 }37 try {38 const token = makeJwt();39 const { data } = await axios.post(40 `${BASE_URL}/apps/${APP_ID}/conversations/${conversationId}/messages`,41 { author: { type: 'business' }, content: { type: 'text', text: messageText } },42 { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } }43 );44 return res.status(200).json({ messageId: data.message?.id });45 } catch (e) {46 return res.status(e.response?.status || 500).json({ error: e.message });47 }48});4950// Inbound: Sunshine webhook → Firestore51exports.sunshineWebhook = functions.https.onRequest(async (req, res) => {52 const event = req.body;53 const messages = event?.events?.[0]?.payload?.messages || [];54 const convId = event?.events?.[0]?.payload?.conversation?.id;55 for (const msg of messages) {56 if (msg.author?.type === 'user' && convId) {57 await db.collection('conversations').doc(convId)58 .collection('messages').add({59 text: msg.content?.text || '',60 author: msg.author?.userId || 'customer',61 timestamp: admin.firestore.FieldValue.serverTimestamp(),62 direction: 'inbound'63 });64 }65 }66 return res.status(200).send('ok');67});Pro tip: JWT tokens signed for Sunshine Conversations expire quickly (60 seconds in this example). Never cache or reuse them across requests — sign a fresh JWT for every outbound API call inside the Cloud Function. This way, even if a token is somehow captured in transit, it expires almost immediately.
Expected result: Both Cloud Functions are deployed. The sendSunshineMessage URL is live and returns a 200 when called with test data. The sunshineWebhook URL is configured in the Sunshine dashboard as the webhook target.
Set up Firestore structure and connect FlutterFlow
Your FlutterFlow app needs a Firestore collection structure to store conversation messages. Open your Firebase console → Firestore → create a collection called `conversations`. Inside it, each document will be a conversation (keyed by Sunshine conversation ID), containing a sub-collection called `messages`. Each message document has: `text` (String), `author` (String), `timestamp` (Timestamp), `direction` (String — 'inbound' or 'outbound'). In FlutterFlow, go to **Settings & Integrations → Firebase** (if not already connected, follow the setup wizard to connect your Firebase project). Once connected, go to **Firestore** in the left nav → click **Import from Firestore** to import your schema. FlutterFlow will detect the `conversations` and `messages` collections. On your chat screen in FlutterFlow, add a **ListView** widget. In its backend settings, set the data source to **Firestore Collection** → `conversations/{conversationId}/messages`, ordered by `timestamp` ascending. Enable 'Listen for Real Time Updates' (the streaming icon next to the query) — this is the key that makes messages appear instantly without polling. Bind each list tile's Text widget to the `text` field, a secondary text to `author`, and a timestamp Text to `timestamp`. Add a visual distinction for inbound vs outbound messages by checking the `direction` field: align inbound messages left, outbound right, with different background colors.
Pro tip: Firestore real-time listeners in FlutterFlow (the 'Listen for Real Time Updates' toggle) consume Firestore read quota for each update. For production, scope the query to a specific conversation ID (pass it as a parameter to the screen) rather than loading all messages from all conversations at once.
Expected result: The FlutterFlow chat screen shows existing messages from Firestore on load and automatically displays new messages as they arrive — both inbound (from customers on external channels) and outbound (sent via your Cloud Function).
Create the FlutterFlow API Group for outbound messages
Click **API Calls** in the FlutterFlow left nav, then **+ Add** → **Create API Group**. Name it 'Sunshine Proxy'. Set the Base URL to your `sendSunshineMessage` Cloud Function URL (e.g., `https://us-central1-your-project.cloudfunctions.net`). No shared headers needed — your Cloud Function handles JWT signing internally. Add an API Call inside the group: click **+ Add API Call**, name it 'Send Message'. Set method to **POST**, path to `/sendSunshineMessage`. In the Body section, type = JSON, with two fields: `conversationId` and `messageText`. Create matching variables in the Variables tab. Also create a second API Group named 'Sunshine Direct' pointed at the actual Sunshine base URL (`https://api.smooch.io/v2`) — you'll use this in future steps if you want to create conversations or list them. For now, the main send path goes through the Cloud Function. On the chat compose row (a TextField + Send button at the bottom of the screen), wire the Send button's Action Flow to: (1) API Request → 'Sunshine Proxy / Send Message', passing the current conversation ID (from Page State or passed as a page parameter) and the TextField value as `messageText`. (2) On success: add an outbound message document directly to Firestore via a **Firestore → Add Document** action (at `conversations/{convId}/messages`) with `text`, `author: 'business'`, `direction: 'outbound'`, `timestamp: currentTime`. (3) Clear the TextField. Adding the outbound message to Firestore locally makes it appear in the real-time ListView immediately without waiting for a round-trip through Sunshine.
1{2 "group": "Sunshine Proxy",3 "baseUrl": "https://us-central1-YOUR-PROJECT.cloudfunctions.net",4 "calls": [5 {6 "name": "Send Message",7 "method": "POST",8 "path": "/sendSunshineMessage",9 "bodyType": "JSON",10 "body": {11 "conversationId": "{{conversationId}}",12 "messageText": "{{messageText}}"13 },14 "variables": [15 { "name": "conversationId", "type": "String" },16 { "name": "messageText", "type": "String" }17 ]18 }19 ]20}Pro tip: If you'd rather not build the JWT proxy and Firestore relay yourself, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.
Expected result: The Send button triggers the Cloud Function, which sends the message via Sunshine Conversations. The message appears in the Firestore sub-collection and immediately shows in the real-time ListView.
Connect channel integrations and test the full round-trip
With the sending and webhook relay working, connect your external channels in the Sunshine Conversations dashboard. Go to your App → Integrations → click 'Add Channel'. Available channels include WhatsApp Business (requires a Meta Business account), SMS (via Twilio or Sunshine's native SMS), Facebook Messenger (requires a Facebook Page), and Instagram Direct. For each channel, follow the in-dashboard connection wizard. WhatsApp requires a verified Meta Business account and phone number; SMS requires a Twilio account or purchasing a Sunshine SMS number; Facebook/Instagram require connecting your business pages. Note: configuring channels requires prerequisites outside this tutorial — see each channel's documentation. Channel configuration is done entirely in the Sunshine dashboard, not in FlutterFlow. To test the full round-trip: use Sunshine's built-in Web Messenger (available in App → Integrations → Web Messenger) to create a test conversation from a browser. Send a message via the Web Messenger — this triggers your `sunshineWebhook` Cloud Function. Verify that the message appears in Firestore, then in your FlutterFlow app's chat screen. Now send a reply from FlutterFlow — confirm it arrives in the Web Messenger browser window. For WhatsApp testing specifically, Sunshine's API lets you create a test conversation directly: call `POST /v2/apps/{appId}/conversations` via your Cloud Function or Postman, then send a message. WhatsApp has template message requirements for the first business-initiated contact outside of a 24-hour window — these templates are managed in the Meta Business Manager, not in FlutterFlow.
Pro tip: When testing webhooks locally, use ngrok to expose your Cloud Function emulator to the public internet (`firebase emulators:start --only functions`, then ngrok http 5001). Register the ngrok URL as the webhook in Sunshine for development testing. Switch to the real deployed URL before going to production.
Expected result: Messages sent from an external channel (Web Messenger, WhatsApp, etc.) appear in the FlutterFlow chat screen via Firestore real-time streaming. Replies sent from FlutterFlow arrive on the external channel. The round-trip works end-to-end.
Common use cases
Customer support chat in a FlutterFlow service app
A FlutterFlow home services app lets customers start a support conversation from within the app. The chat screen binds to a Firestore conversation thread and sends messages via the Sunshine Conversations API. Support agents reply via Zendesk's Agent Workspace and customers see responses in real-time inside the app.
Add a support chat screen where users can send messages to our support team and see replies in real time, with the conversation persisted across app restarts.
Copy this prompt to try it in FlutterFlow
WhatsApp outreach tool for a sales team
A FlutterFlow B2B sales app lets sales reps send WhatsApp messages to leads directly from the CRM. The rep selects a lead, types a message, and the app sends it via Sunshine Conversations' WhatsApp channel integration. Replies arrive via Firestore and appear in the lead's conversation thread.
Let sales reps send and receive WhatsApp messages from within the app, with messages linked to the corresponding lead record.
Copy this prompt to try it in FlutterFlow
Multi-channel inbox for a FlutterFlow admin dashboard
A FlutterFlow admin app aggregates messages from WhatsApp, SMS, and Facebook Messenger into a unified inbox. Each conversation thread is stored in Firestore (written by the webhook Cloud Function) and displayed as a list. Tapping a thread opens the chat and allows the admin to reply on the original channel.
Build an inbox screen showing all incoming messages from WhatsApp, SMS, and Messenger in one list, with a detail view that lets admins reply.
Copy this prompt to try it in FlutterFlow
Troubleshooting
Cloud Function returns 401 when calling the Sunshine Conversations API
Cause: Likely a JWT signing error — the Key ID in the JWT header doesn't match the Key ID stored in Sunshine, the JWT has expired, or the base URL region doesn't match your Sunshine account's data region.
Solution: Verify the Key ID (the `kid` field in the JWT header) exactly matches the Key ID shown in your Sunshine App → API Keys panel. Check that your Cloud Function config has the correct region (`sunshine.region = 'eu'` for EU accounts), and that it's using `api.eu-1.smooch.io` not `api.smooch.io` for EU. Sign a fresh JWT for each request — never reuse cached tokens. Use jwt.io to decode the token your function is generating and verify the header and payload look correct.
Inbound messages from customers are not appearing in the FlutterFlow chat screen
Cause: Either the Sunshine webhook is not configured correctly (wrong URL or not enabled for the right events), the Cloud Function is failing to write to Firestore, or the FlutterFlow Firestore stream is querying the wrong collection path.
Solution: Check the Sunshine dashboard → Webhooks to confirm the webhook URL is pointing to your `sunshineWebhook` function and the `message:appUser` trigger is enabled. Open Firebase Functions Logs and look for errors from `sunshineWebhook` — common issues include Firestore permission errors (check Firestore rules allow the function to write) or JSON parsing errors if the Sunshine webhook payload structure changed. Also verify that the FlutterFlow Firestore query uses the same conversation ID path as what the webhook writes to.
'XMLHttpRequest error' when testing the API Call to the Cloud Function in FlutterFlow web Run mode
Cause: The Cloud Function is not returning CORS headers that allow the FlutterFlow web preview origin to make cross-origin requests.
Solution: Add the CORS headers to your `sendSunshineMessage` Cloud Function as shown in the code example in Step 2 — specifically `Access-Control-Allow-Origin: *` on all responses and a 204 response for OPTIONS preflight requests. The Cloud Function example in this tutorial already includes the CORS handler. If you've customized the function, confirm both the preflight OPTIONS handler and the main response set the header.
EU region calls return 404 Not Found even though the App ID and credentials are correct
Cause: The Cloud Function is using the US base URL (api.smooch.io) when the Sunshine App is in the EU region (api.eu-1.smooch.io). App IDs in one region do not exist in the other.
Solution: In your Firebase config, set `sunshine.region = 'eu'` and redeploy the Cloud Functions. The function code reads this config value to select the correct base URL. Also confirm in the Sunshine dashboard under App Settings or your Zendesk admin panel what data region your organization is on — this should match.
Best practices
- Never sign Sunshine Conversations JWTs in Dart on the device — the Key Secret would be shipped inside the compiled app binary and is decompilable; always sign server-side in your Cloud Function
- FlutterFlow cannot receive webhooks — always use a Cloud Function endpoint for inbound Sunshine events and relay them to Firestore for real-time display
- Sign a fresh JWT per outbound API request — JWT expiry is short (60 seconds is reasonable) to limit the blast radius if a token is intercepted
- Use Firestore real-time listeners (the 'Listen for Real Time Updates' toggle in FlutterFlow) for the chat ListView — this is the key to a live-feeling chat UI without polling
- Configure WhatsApp and other channel connectors entirely in the Sunshine dashboard; FlutterFlow only handles the send/receive of message content, not channel-specific configuration like templates or sender IDs
- Add both directions to Firestore (write outbound messages locally when the user sends, write inbound via webhook) to keep the chat history complete even if a network blip delays a Sunshine delivery receipt
- Select the correct data region (US vs EU) when creating your Sunshine App — you cannot migrate between regions without recreating the App, and the base URL must match
Alternatives
Telegram has an official, free bot API with a simple token — no JWT signing, no Cloud Function needed for basic sends; choose Telegram for single-channel messaging without a Zendesk dependency.
Zendesk Chat is the embedded live-chat product within Zendesk (formerly Zopim), designed for website/app chat with agents — use it instead of Sunshine if you only need in-app chat and don't need omnichannel (WhatsApp/SMS) unification.
Vonage's Messages API also provides WhatsApp, SMS, and Viber in one API but with a different auth model (JWT from app private key) — use Vonage if you want omnichannel without a full Zendesk subscription.
Frequently asked questions
Is Smooch the same as Zendesk Sunshine Conversations?
Yes — Smooch was the original company and product name. Zendesk acquired Smooch in 2019 and rebranded it as Sunshine Conversations, integrating it into the Zendesk Suite. The API base URL still uses `api.smooch.io` and you'll see 'Smooch' referenced in older documentation and SDK names, but the product is now officially called Zendesk Sunshine Conversations.
Can FlutterFlow users start a new conversation, or do conversations need to be created server-side?
You can create conversations via the Sunshine Conversations v2 API (`POST /v2/apps/{appId}/conversations`) — route this through your Cloud Function the same way you route message sends. For user-initiated in-app chat, you typically create a conversation when the user taps 'Start Chat' and store the returned conversation ID in Firestore linked to the user's profile, so subsequent messages use the same conversation ID.
Does this integration support sending images or attachments, not just text?
Yes — the Sunshine Conversations message body supports different content types. Instead of `{type: 'text', text: '...'}`, you can send `{type: 'image', mediaUrl: 'https://...'}` or `{type: 'file', mediaUrl: '...', mediaType: 'application/pdf'}`. Upload the file to Firebase Storage or Supabase Storage first, get the public URL, then include it in the message content body sent through your Cloud Function.
How do I know which channel a customer message came from?
The Sunshine webhook payload includes a `channel` field on the conversation object (e.g., `whatsapp`, `messenger`, `sms`, `web`). Your `sunshineWebhook` Cloud Function can extract this and store it in the Firestore message document as a `channel` field. In FlutterFlow, you can then display a channel badge on each inbound message — for example, a WhatsApp logo for WhatsApp messages.
What happens if my Cloud Function is slow and the JWT expires before the Sunshine API call completes?
The JWT in the example code is set to expire in 60 seconds, and a Cloud Function call to Sunshine typically completes in under 2 seconds — so expiry mid-request is not a real risk in practice. If you're seeing 401 errors mid-call, check that the system clock on your Cloud Function host is accurate (NTP drift can cause JWT timestamp validation to fail). Google Cloud Functions use NTP-synchronized clocks, so clock drift is typically not an issue.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation