# Sinch

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Sinch by routing messages through a Firebase Cloud Function that holds your Sinch API credentials and calls the Sinch Conversation API. Because Sinch's OAuth Access Tokens expire roughly every hour, refreshing them client-side is impractical — the Cloud Function handles token management. Your FlutterFlow app calls the function with the recipient, message text, and an optional channel priority order (e.g., RCS then SMS fallback), consuming Sinch messaging credits per send.

## Sinch Conversation API and the Cloud Function Requirement

Sinch's Conversation API provides a unified endpoint to reach users across SMS, RCS, WhatsApp, MMS, and more — all from a single POST /messages:send call. The key differentiator is channel_priority_order: you specify a list like ["RCS","SMS"] and Sinch automatically tries RCS first, falling back to SMS if the recipient's device does not support it. This removes the need to detect device capabilities in your app.

Authentication uses either a static API Token (Service Plan) or OAuth 2.0 (Key ID and Key Secret). With OAuth, access tokens expire approximately every hour — requesting a new token requires your Key Secret. Placing the Key Secret inside a FlutterFlow API Call header would compile it into the app binary, where it can be extracted with widely available tools. A Firebase Cloud Function solves both problems: it holds the Key Secret securely in environment config and fetches a fresh OAuth token automatically before each send.

Sinch offers a free trial with a limited credit balance to test messaging across channels. After the trial, billing is per-message, per-channel, and per-region — check current rates on the Sinch pricing page before planning bulk campaigns. Each channel (SMS, RCS, WhatsApp) also has its own onboarding: for SMS you need a verified sender ID or phone number; for WhatsApp you need approved message templates; for RCS you need carrier registration in your target markets.

## Before you start

- A Sinch account at sinch.com with a Project ID, Access Key ID, and Key Secret from the Customer Dashboard
- A verified sender ID or phone number for SMS, and completed channel onboarding for any other channels (RCS, WhatsApp) you want to use
- Firebase project on the Blaze (pay-as-you-go) plan with Cloud Functions enabled — required for outbound HTTP calls to Sinch
- A FlutterFlow project with at least one screen and a Firestore database set up for logging sent messages
- Basic Node.js familiarity to edit and deploy the Cloud Function

## Step-by-step guide

### 1. Get your Sinch credentials from the Customer Dashboard

Log in to your Sinch account at dashboard.sinch.com. In the left sidebar, navigate to **Settings** → **Access Keys**. Click **New Key** to create an access key pair — you'll receive a **Key ID** (which you can view later) and a **Key Secret** (shown only once — copy it immediately into a secure location like a password manager).

Next, note your **Project ID** — it's visible at the top of the Customer Dashboard under your account name. You'll need this for all Conversation API calls.

Navigate to **Conversation API** → **Apps** in the left sidebar and create a Conversation API app. Give it a name and enable the channels you plan to use (SMS, RCS, WhatsApp). For SMS, go to **Numbers** → **Get a Number** to provision a sender number. For WhatsApp, you'll need to go through Meta's Business Manager integration. For RCS, carrier registration is required per target market — Sinch's team can assist.

Keep the Key ID, Key Secret, Project ID, and your Sinch App ID handy — all four will be used in the Cloud Function environment configuration. Never paste these into FlutterFlow.

**Expected result:** You have your Sinch Project ID, Key ID, Key Secret, and Conversation App ID saved securely and your chosen channels are provisioned.

### 2. Deploy a Firebase Cloud Function that proxies sends and manages OAuth tokens

Create a Firebase Cloud Function that handles two responsibilities: acquiring a fresh Sinch OAuth access token and forwarding your FlutterFlow app's send requests to the Sinch Conversation API. Open the Firebase Console for your project, confirm you're on the Blaze plan (free Spark blocks outbound HTTP), and navigate to Functions.

In your `functions/index.js`, configure the four Sinch credentials as Firebase environment variables using the Firebase CLI:
```
firebase functions:config:set sinch.key_id="YOUR_KEY_ID" sinch.key_secret="YOUR_KEY_SECRET" sinch.project_id="YOUR_PROJECT_ID" sinch.app_id="YOUR_APP_ID"
```
Then deploy the function shown below. The function first fetches a short-lived OAuth token from Sinch's auth endpoint, then posts to the Conversation API's send endpoint using that token. Token refresh is fully automatic — no client-side timer needed.

The `channel_priority_order` array in the request body is the Sinch Conversation API's key differentiator: list channels in priority order and Sinch will try each in sequence until one succeeds. An order of `["RCS","SMS"]` means every capable Android gets an RCS message; everyone else gets SMS — zero additional code required on your side.

After deploying, copy the HTTPS trigger URL from the Firebase Console (Functions dashboard → your function name → Trigger URL). This is what FlutterFlow will call.

```
// functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');

admin.initializeApp();
const db = admin.firestore();

const KEY_ID = functions.config().sinch.key_id;
const KEY_SECRET = functions.config().sinch.key_secret;
const PROJECT_ID = functions.config().sinch.project_id;
const APP_ID = functions.config().sinch.app_id;
const SINCH_AUTH_URL = 'https://auth.sinch.com/oauth2/token';
const SINCH_SEND_URL = `https://us.conversation.api.sinch.com/v1/projects/${PROJECT_ID}/messages:send`;

async function getSinchToken() {
  const credentials = Buffer.from(`${KEY_ID}:${KEY_SECRET}`).toString('base64');
  const response = await axios.post(SINCH_AUTH_URL,
    'grant_type=client_credentials',
    {
      headers: {
        Authorization: `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );
  return response.data.access_token;
}

exports.sinchSend = functions.https.onRequest(async (req, res) => {
  if (req.method !== 'POST') return res.status(405).send('Method Not Allowed');

  const { to, text, channelPriority } = req.body;
  if (!to || !text) {
    return res.status(400).json({ error: 'to and text are required' });
  }

  try {
    const token = await getSinchToken();
    const payload = {
      app_id: APP_ID,
      recipient: { identified_by: { channel_identities: [{ channel: 'SMS', identity: to }] } },
      message: { text_message: { text } },
      channel_priority_order: channelPriority || ['RCS', 'SMS']
    };
    const response = await axios.post(SINCH_SEND_URL, payload, {
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
    });
    // Log message_id to Firestore
    await db.collection('sinch_messages').add({
      to,
      text,
      messageId: response.data.message_id,
      sentAt: admin.firestore.FieldValue.serverTimestamp()
    });
    return res.status(200).json({ message_id: response.data.message_id });
  } catch (err) {
    const status = err.response ? err.response.status : 500;
    return res.status(status).json({ error: err.message });
  }
});
```

**Expected result:** Firebase Console shows the `sinchSend` function deployed with a green checkmark and a working HTTPS trigger URL.

### 3. Create the API Call in FlutterFlow targeting your Cloud Function

In the FlutterFlow editor, click **API Calls** in the left navigation panel. Click **+ Add** → **Create API Group**. Name it `SinchProxy` and set the Base URL to your Firebase Cloud Function URL (the base, e.g. `https://us-central1-yourproject.cloudfunctions.net`).

Inside the group, click **+ Add API Call** and name it `SendMessage`. Set the method to `POST` and the endpoint to `/sinchSend`. In the **Variables** tab, add three variables:
- `to` (String) — the recipient's phone number in E.164 format (e.g., `+14155551234`)
- `text` (String) — the message body
- `channelPriority` (String, optional) — comma-separated channels in priority order

In the **Body** section, choose **JSON** and enter:
```json
{"to":"{{to}}","text":"{{text}}","channelPriority":["RCS","SMS"]}
```

Switch to **Response & Test** and paste a sample response: `{"message_id":"01ABC123"}`. Click **Generate JSON Paths** — FlutterFlow will create a path `$.message_id` that you can use in action chains to confirm and log the send.

Click **Test API Call**, fill in a real phone number for `to` (in E.164 format) and a test message for `text`, then click **Test**. If your Cloud Function is deployed correctly and Sinch credentials are valid, you'll see `{"message_id":"..."}` in the response — and receive the SMS on your phone within seconds. If you see a 401, the token fetch failed (check Key ID/Secret). If you see a 400, check the phone number format.

Do not add any `Authorization` header in FlutterFlow's API Call — authentication is entirely handled inside the Cloud Function.

```
// FlutterFlow API Call configuration
{
  "group_name": "SinchProxy",
  "base_url": "https://us-central1-yourproject.cloudfunctions.net",
  "calls": [
    {
      "name": "SendMessage",
      "method": "POST",
      "endpoint": "/sinchSend",
      "headers": {},
      "body_type": "JSON",
      "body": {
        "to": "{{to}}",
        "text": "{{text}}",
        "channelPriority": ["RCS", "SMS"]
      },
      "variables": [
        { "name": "to", "type": "String" },
        { "name": "text", "type": "String" }
      ]
    }
  ]
}
```

**Expected result:** The API Call test returns `{"message_id":"..."}` and an SMS (or RCS message) arrives on the test phone number within a few seconds.

### 4. Add a confirmation dialog and wire the send action to a button

Sinch messaging credits are consumed on send and cannot be recovered — a misfire (typo in the number, test spam, or accidental double-tap) costs real money. Before wiring the send directly to a button, add a confirmation step.

In your FlutterFlow screen, add a `TextField` for the recipient's phone number, a `TextField` for the message body, and a `Button` labeled 'Send Message'. Select the Button, open the **Actions** panel, and click **+ Add Action**. Choose **Alert Dialog** → **Confirm Dialog** with the message 'Are you sure you want to send this message? Messaging credits will be consumed.' Set the confirm label to 'Send' and the cancel label to 'Cancel'.

As the 'On Confirm' chained action, add **Backend/API Call** → **SinchProxy → SendMessage**. Bind:
- `to`: the phone number `TextField`'s current value (or a Firestore field for the selected recipient)
- `text`: the message `TextField`'s current value

As a second chained action after the API call, add **Show Snack Bar**: if `$.message_id` is non-empty, show 'Message sent!' — otherwise show 'Send failed. Check the phone number and try again.'

For apps that also need to display sent message history, add a **Firestore Query** on the `sinch_messages` collection (ordered by `sentAt` descending) to a `ListView` below the form, so users can see their recent sends without leaving the app.

**Expected result:** Tapping the button shows a confirmation dialog; confirming sends the message and shows 'Message sent!' — the `sinch_messages` Firestore collection gains a new document with the message_id.

### 5. Handle inbound delivery receipts and configure Sinch webhooks

Sinch can push delivery receipts (delivered, failed, queued) and inbound replies back to a webhook — useful for showing delivery status in your app or handling two-way conversations. Since your FlutterFlow app can't receive HTTP requests, this webhook must be your Firebase Cloud Function.

In the Sinch Customer Dashboard, navigate to **Conversation API** → **Apps** → your app → **Webhooks**. Click **Add Webhook** and enter the URL of your Cloud Function's webhook handler (you'll add a second export to `index.js` for this). Select the event triggers: `MESSAGE_DELIVERY` for delivery receipts and `INBOUND_MESSAGE` for replies.

Update your Cloud Function to handle these events: on `MESSAGE_DELIVERY`, update the corresponding Firestore document's status field; on `INBOUND_MESSAGE`, write a new document to a `sinch_inbound` collection. Your FlutterFlow app can then subscribe to `sinch_inbound` with a real-time Firestore query to display replies in an inbox screen.

If you'd rather skip the Cloud Function and webhook setup entirely, RapidDev's team handles FlutterFlow integrations like this every week — book a free scoping call at rapidevelopers.com/contact.

```
// Add to functions/index.js — webhook handler for delivery receipts and inbound messages
exports.sinchWebhook = functions.https.onRequest(async (req, res) => {
  const event = req.body;
  const type = event.type;

  if (type === 'MESSAGE_DELIVERY') {
    const { message_id, status } = event.message_delivery_report || {};
    if (message_id) {
      const snap = await db.collection('sinch_messages')
        .where('messageId', '==', message_id).limit(1).get();
      if (!snap.empty) {
        await snap.docs[0].ref.update({ deliveryStatus: status });
      }
    }
  } else if (type === 'INBOUND_MESSAGE') {
    const msg = event.message;
    await db.collection('sinch_inbound').add({
      from: event.contact_id,
      text: msg && msg.text_message ? msg.text_message.text : '',
      receivedAt: admin.firestore.FieldValue.serverTimestamp()
    });
  }

  res.status(200).send('ok');
});
```

**Expected result:** Delivery status updates appear in the `sinch_messages` Firestore documents and inbound replies appear in the `sinch_inbound` collection, which your FlutterFlow app can display in real time.

## Best practices

- Never place Sinch Key ID, Key Secret, or Project ID in a FlutterFlow API Call — these credentials compile into the app bundle. Always keep them in Firebase Cloud Function environment config.
- Refresh the OAuth Access Token inside the Cloud Function on every request (or cache it with a 50-minute TTL). OAuth tokens expire in approximately one hour — a stale token causes 401 errors that are hard to debug from the app side.
- Always add a confirmation dialog in FlutterFlow before triggering a Sinch send — credits are consumed on send and cannot be refunded. Even a typo in a phone number costs money.
- Log the `message_id` returned by Sinch to Firestore immediately after a successful send. This allows you to correlate delivery receipts from the webhook with outbound messages.
- Use `channel_priority_order` to define fallback logic (e.g., `["RCS","SMS"]`) rather than hard-coding a single channel. This ensures maximum deliverability without extra code.
- Complete channel onboarding before testing end-to-end. Each channel (SMS, RCS, WhatsApp) requires separate setup in the Sinch Dashboard — sending on an un-onboarded channel results in QUEUED messages that never deliver.
- Store phone numbers in E.164 format (`+[country code][number]`) in your Firestore database. Validate the format before calling the API to prevent wasting credits on malformed numbers.
- For high-volume sends (hundreds of messages), fan out from the Cloud Function itself using batch logic rather than triggering one FlutterFlow API Call per recipient — this avoids UI timeouts and reduces Firestore write overhead.

## Use cases

### Two-factor authentication via SMS with RCS upgrade

A fintech or marketplace app needs to verify users' phone numbers during sign-up. The app calls a Cloud Function that sends a one-time code via RCS (rich, branded message) on Android and falls back to SMS for all other devices — all with a single `channel_priority_order: ["RCS", "SMS"]` configuration.

Prompt example:

```
Build a phone verification screen that sends a 6-digit OTP to the user's phone number using Sinch, showing a branded RCS message on Android and a plain SMS on other devices.
```

### Order dispatch notifications

A logistics or food delivery app triggers a Sinch send when an order's status changes to 'Out for Delivery'. The message includes the estimated arrival time and a tracking link, delivered to the customer's phone number via the highest available channel — reducing missed deliveries without requiring the customer to have the app open.

Prompt example:

```
When an order status updates to 'dispatched', automatically send an SMS/RCS notification to the customer's phone number with the order number, ETA, and a tracking link.
```

### Appointment reminders for a booking platform

A healthcare or services booking app stores appointments in Firestore. A scheduled Cloud Function runs daily, finds appointments in the next 24 hours, and sends Sinch messages to each patient — reducing no-shows. The `channel_priority_order` ensures delivery even if the patient's phone doesn't support RCS.

Prompt example:

```
Create a reminder system that sends a Sinch message to patients 24 hours before their appointment, including the appointment time, provider name, and a link to reschedule.
```

## Troubleshooting

### Cloud Function returns 401 Unauthorized on every send

Cause: The Sinch OAuth token fetch is failing — most likely because the Key ID or Key Secret in Firebase environment config is incorrect, or the access key has been disabled in the Sinch Dashboard.

Solution: In the Firebase Console, go to Functions → Configuration and verify the `sinch.key_id` and `sinch.key_secret` values match exactly what's shown in the Sinch Customer Dashboard under Settings → Access Keys. If you suspect the secret was lost, create a new access key (the old one cannot recover its secret), update the Firebase config with `firebase functions:config:set sinch.key_secret="NEW_SECRET"`, and redeploy.

### API returns 404 Not Found when calling the Conversation API send endpoint

Cause: The Project ID or App ID is incorrect, or you're using the wrong region host. Sinch has separate US and EU API hosts — calling `us.conversation.api.sinch.com` for an EU-region project returns 404.

Solution: Check your project's region in the Sinch Customer Dashboard (Settings → API). If your project is EU-based, update the `SINCH_SEND_URL` in your Cloud Function to `https://eu.conversation.api.sinch.com/v1/projects/${PROJECT_ID}/messages:send` and redeploy. Also confirm the Project ID and App ID in your function config match the dashboard values exactly.

```
// Change the region host in your Cloud Function
const SINCH_SEND_URL = `https://eu.conversation.api.sinch.com/v1/projects/${PROJECT_ID}/messages:send`;
```

### Message sends but is never delivered — status stays QUEUED

Cause: The channel (e.g. SMS) is not fully onboarded for the target country. For SMS, you need a verified sender ID or provisioned phone number. For WhatsApp, you need pre-approved message templates. For RCS, you need carrier registration.

Solution: In the Sinch Dashboard, navigate to **Conversation API** → **Apps** → your app and check the channel health indicators. For SMS, verify you have a provisioned number under **Numbers** for the recipient's country. If you see 'sender not approved' errors in Firestore or function logs, contact Sinch support to complete channel onboarding for your target markets.

### FlutterFlow API Call test shows XMLHttpRequest error or network error in web preview

Cause: CORS (Cross-Origin Resource Sharing) headers are missing from the Firebase Cloud Function response. Browser-based requests (including FlutterFlow's web preview and Run mode) require CORS headers; native iOS/Android builds do not.

Solution: Install the `cors` npm package in your functions directory and wrap the function handler. Redeploy after adding the CORS middleware.

```
const cors = require('cors')({ origin: true });
exports.sinchSend = functions.https.onRequest((req, res) => {
  cors(req, res, async () => {
    // ... existing handler code ...
  });
});
```

## Frequently asked questions

### Why does the Sinch OAuth token expire every hour and why can't I handle that in FlutterFlow?

Sinch's OAuth access tokens have a short expiry window (approximately one hour) as a security measure — a leaked token has limited blast radius. Refreshing requires your Key Secret, which must never leave a secure server environment. Because FlutterFlow compiles to a client app, storing the Key Secret there would expose it in the app binary. The Firebase Cloud Function refreshes the token automatically on every send, so your app never has to manage token lifecycle.

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

Yes, with additional setup. WhatsApp through Sinch requires you to connect your WhatsApp Business Account in the Sinch Dashboard and create pre-approved message templates in Meta's Business Manager. Once onboarded, you add `"WHATSAPP"` to your `channel_priority_order` array and Sinch handles routing. Note that WhatsApp only allows template messages for outbound sends outside of a 24-hour reply window — free-form text only works within 24 hours of a user's last message.

### Do I need a separate phone number for each country I want to send SMS to?

Not necessarily. Sinch can provision shared or long-code numbers that work internationally, but deliverability varies by destination country — some countries require local sender IDs. For high-volume sending to specific countries, provisioning a local virtual number or applying for an approved Sender ID improves deliverability. Check the Sinch Dashboard under Numbers → Search for availability in your target markets.

### Can my FlutterFlow app receive inbound SMS replies from users?

Not directly — FlutterFlow apps have no server address to receive HTTP webhooks. The pattern is to register your Firebase Cloud Function URL as a Sinch webhook for `INBOUND_MESSAGE` events. When a user replies, Sinch calls your function, which writes the message to a Firestore collection. Your FlutterFlow app then displays the conversation using a real-time Firestore query, updating instantly as new messages arrive.

---

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