# How to Integrate Bolt.new with Sinch

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 20 minutes
- Last updated: April 2026

## TL;DR

To integrate Sinch with Bolt.new, create Next.js API routes that call the Sinch REST API with your Service Plan ID and API token. Sinch offers SMS, MMS, RCS (rich Android messaging), and number verification. Outbound messages and OTP verification work in Bolt's WebContainer. Inbound message webhooks require a deployed URL — deploy to Netlify or Bolt Cloud before configuring inbound callbacks.

## SMS, MMS, and RCS Messaging with Sinch in Bolt.new

Sinch is a global communications platform that has grown significantly through acquisitions, owning Mailgun, Mailjet, and SparkPost on the email side, and offering SMS, MMS, RCS, voice, and verification APIs on the messaging side. For Bolt.new developers, Sinch's main differentiator is RCS (Rich Communication Services) — the next generation of SMS that enables branded messages with images, carousels, quick reply buttons, and read receipts on Android devices. While standard SMS still works on all phones, RCS provides an app-like experience without requiring users to download anything.

Sinch's SMS API uses straightforward HTTP Basic authentication: your Service Plan ID as the username and your API token as the password. No OAuth dance, no token expiry management — just credentials in an Authorization header. This simplicity makes the integration clean to implement in Bolt.new: create a Next.js API route, add the credentials to your .env file, and make fetch() calls to Sinch's REST endpoints. The Sinch SDK (`@sinch/sdk-core`) is also available as an alternative to raw fetch calls if you prefer a strongly-typed SDK approach — it is pure JavaScript and works in Bolt's WebContainer.

Sinch Verification is a separate product within the Sinch ecosystem for phone number verification (OTP). It sends a one-time password via SMS (or voice) to verify that a user controls a phone number. The Verification API is separate from the SMS API with different credentials and endpoint format. This tutorial covers both the messaging API and the Verification API.

## Before you start

- A Sinch account (free trial includes credit for testing)
- A Sinch Service Plan created in the Sinch Customer Dashboard for SMS
- Service Plan ID and API token from the Sinch Dashboard
- A Bolt.new project using Next.js for server-side API routes
- For RCS: a Sinch project with RCS agent configured (requires Sinch RCS approval)

## Step-by-step guide

### 1. Get Sinch API Credentials

Sinch has a somewhat complex credential structure because it has multiple product lines (acquired companies) with different authentication models. For SMS, you need credentials from the Sinch Customer Dashboard at dashboard.sinch.com — these are different from the newer Sinch Platform (sinch.com/dashboard) used for other services.

For SMS API credentials: log into dashboard.sinch.com → go to SMS → Service Plans. If you don't have a service plan, create one (US service plan for North American numbers). Click on your service plan to see its details. You need two values: the 'Service Plan ID' (a UUID like 'a5a17e88-...') and the 'API Token' (a long alphanumeric string). Copy both.

You also need a sender number (a virtual number or short code). In the Service Plan, go to 'Numbers' and add a US number to your service plan. Trial accounts get a shared number for testing.

For Sinch Verification API: this uses a different credential set — an Application Key and Application Secret. Go to dashboard.sinch.com → Verification → Apps. Create a new verification app and note the Application Key (app_key) and Application Secret (app_secret). These are separate from your SMS credentials and cannot be used interchangeably.

```
# Sinch SMS API authentication (HTTP Basic Auth):
# Username: Service Plan ID (UUID from SMS → Service Plans)
# Password: API Token (long alphanumeric string)

# Sinch SMS API base URL:
# US: https://us.sms.api.sinch.com/xms/v1/{servicePlanId}/
# EU: https://eu.sms.api.sinch.com/xms/v1/{servicePlanId}/

# Sinch Verification API base URL:
# https://verificationapi-v1.sinch.com/verification/v1/
# Auth: Basic {base64(app_key:app_secret)}
```

**Expected result:** You have your Sinch Service Plan ID, API Token, and optionally Verification App Key and Secret. You also have a sender phone number associated with your service plan.

### 2. Configure Environment Variables and Create Sinch Client

Set up environment variables for all the Sinch credentials you need. Sinch SMS and Verification use different credential sets, so name them clearly to avoid confusion. All credentials are server-side only — never add NEXT_PUBLIC_ prefix to any Sinch credential.

For the SMS API, Sinch uses HTTP Basic authentication where the username is your Service Plan ID and the password is your API Token. The Authorization header value is 'Basic ' + base64(servicePlanId + ':' + apiToken). All modern fetch calls support this natively via the Authorization header.

Create a lib/sinch.ts client utility that exports functions for the SMS operations you need. Building typed functions for common operations (sendSMS, sendBatch, getMessageStatus) is cleaner than writing raw fetch calls in each API route. Include the service plan ID in the base URL since Sinch's SMS API uses it in the path.

```
// lib/sinch.ts
const SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID!;
const API_TOKEN = process.env.SINCH_API_TOKEN!;
const VERIFICATION_APP_KEY = process.env.SINCH_VERIFICATION_APP_KEY!;
const VERIFICATION_APP_SECRET = process.env.SINCH_VERIFICATION_APP_SECRET!;

export const SINCH_SENDER = process.env.SINCH_SENDER_NUMBER!;
const SMS_BASE = `https://us.sms.api.sinch.com/xms/v1/${SERVICE_PLAN_ID}`;
const VERIFICATION_BASE = 'https://verificationapi-v1.sinch.com/verification/v1';

function basicAuth(user: string, pass: string): string {
  return 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
}

export async function sinchSMSFetch<T>(path: string, method = 'GET', body?: object): Promise<T> {
  const response = await fetch(`${SMS_BASE}${path}`, {
    method,
    headers: {
      Authorization: basicAuth(SERVICE_PLAN_ID, API_TOKEN),
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!response.ok) {
    const text = await response.text();
    throw new Error(`Sinch SMS ${response.status}: ${text}`);
  }
  return response.json() as Promise<T>;
}

export async function sinchVerificationFetch<T>(path: string, method = 'POST', body?: object): Promise<T> {
  const response = await fetch(`${VERIFICATION_BASE}${path}`, {
    method,
    headers: {
      Authorization: basicAuth(VERIFICATION_APP_KEY, VERIFICATION_APP_SECRET),
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!response.ok) {
    const text = await response.text();
    throw new Error(`Sinch Verification ${response.status}: ${text}`);
  }
  return response.json() as Promise<T>;
}
```

**Expected result:** lib/sinch.ts exports authenticated fetch wrappers for both the SMS and Verification APIs. All credentials are in .env as server-side variables.

### 3. Create the SMS Sending API Route

Sinch's SMS API uses a batching model — every send operation creates a 'batch', even for single messages. A batch can have multiple recipients (up to 1,000 in a single call). The batch creation endpoint is POST /batches, and it accepts a from (sender number), to (array of recipient numbers in E.164 format), and body (message text).

Sinch's batch API response includes a batch ID (id), status, and scheduled delivery information. The batch ID can be used to retrieve delivery reports later. Store the batch ID if you want to check delivery status via GET /batches/{batchId}/deliveryreport.

For personalized messages (different text for each recipient), Sinch supports parameterized messages using ${variable_name} syntax in the message body and a parameters object that maps variables to recipient-specific values. This is more efficient than sending N separate API calls for N recipients with different messages.

```
// app/api/sinch/send-sms/route.ts
import { NextResponse } from 'next/server';
import { sinchSMSFetch, SINCH_SENDER } from '@/lib/sinch';

interface SinchBatch {
  id: string;
  type: string;
  status: string;
  to: string[];
  from: string;
  created_at: string;
}

const E164_REGEX = /^\+[1-9]\d{1,14}$/;

export async function POST(request: Request) {
  try {
    const { to, message } = await request.json();
    const recipients = Array.isArray(to) ? to : [to];

    const invalidNumbers = recipients.filter((n: string) => !E164_REGEX.test(n));
    if (invalidNumbers.length > 0) {
      return NextResponse.json(
        { error: `Invalid phone numbers: ${invalidNumbers.join(', ')}` },
        { status: 400 }
      );
    }

    if (!message || message.trim().length === 0) {
      return NextResponse.json({ error: 'Message cannot be empty' }, { status: 400 });
    }

    const batch = await sinchSMSFetch<SinchBatch>('/batches', 'POST', {
      from: SINCH_SENDER,
      to: recipients,
      body: message,
    });

    return NextResponse.json({
      batchId: batch.id,
      status: batch.status,
      recipients: batch.to.length,
    });
  } catch (error: unknown) {
    const e = error as { message: string };
    return NextResponse.json({ error: e.message }, { status: 500 });
  }
}
```

**Expected result:** POST to /api/sinch/send-sms with phone number(s) and message sends real SMS via Sinch. Returns a batch ID. Works in the Bolt preview during development.

### 4. Implement Number Verification OTP

Sinch Verification provides a managed OTP service specifically for phone number verification. Unlike building your own OTP (generate code, send SMS, store and check), Sinch Verification handles the entire lifecycle: code generation, SMS or voice delivery, expiry management, attempt limits, and fraud prevention.

The Verification API flow has two steps. First, initiate verification: POST /verifications/number with the phone number and method ('sms' or 'callout'). Sinch sends the code to the user's phone. The response includes a status of 'PENDING'. Second, verify the code: PUT /verifications/number/{phoneNumber} with the code the user entered. If correct and not expired, the response status is 'SUCCESSFUL'.

Sinch Verification uses different credentials from the SMS API — the app key and app secret from your Verification app. These are different services in Sinch's platform and cannot share credentials. Ensure you have both sets of credentials configured.

```
// app/api/sinch/verify/start/route.ts
import { NextResponse } from 'next/server';
import { sinchVerificationFetch } from '@/lib/sinch';

export async function POST(request: Request) {
  try {
    const { phoneNumber } = await request.json();

    const result = await sinchVerificationFetch('/verifications/number', 'POST', {
      identity: { type: 'number', endpoint: phoneNumber },
      method: 'sms',
    });

    return NextResponse.json({ status: 'pending', result });
  } catch (error: unknown) {
    const e = error as { message: string };
    return NextResponse.json({ error: e.message }, { status: 500 });
  }
}

// app/api/sinch/verify/check/route.ts
export async function POST(request: Request) {
  try {
    const { phoneNumber, code } = await request.json();
    const encoded = encodeURIComponent(phoneNumber);

    const result = await sinchVerificationFetch<{ status: string }>(
      `/verifications/number/${encoded}`,
      'PUT',
      { sms: { code } }
    );

    const verified = result.status === 'SUCCESSFUL';
    return NextResponse.json({ verified, status: result.status });
  } catch (error: unknown) {
    const e = error as { message: string };
    return NextResponse.json({ error: e.message }, { status: 500 });
  }
}
```

**Expected result:** POST to /api/sinch/verify/start sends an OTP SMS to the phone number. POST to /api/sinch/verify/check with the received code returns {verified: true}. Both work in the Bolt preview.

### 5. Deploy and Configure Inbound Message Webhooks

All outbound Sinch operations (sending SMS, verification) work in the Bolt preview. Receiving inbound messages — when users reply to your SMS or text your Sinch number — requires Sinch to send HTTP callbacks to your server. Since Bolt's WebContainer has no public URL, webhooks cannot reach it during development. Deploy first, then configure.

Deploy your Bolt project to Netlify via Settings → Applications → Netlify. After deploying, add your Sinch environment variables in Netlify (Site Settings → Environment Variables): SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_SENDER_NUMBER. Redeploy to apply variables.

To configure inbound message callbacks in Sinch: go to dashboard.sinch.com → SMS → Service Plans → click your plan → Callback URLs. Set the 'Incoming Message Callback URL' to your deployed endpoint (e.g., https://your-app.netlify.app/api/sinch/incoming). Sinch sends POST requests with a JSON body containing the message data when someone replies to your number.

For delivery reports (confirming a message was delivered), set the 'Delivery Report Callback URL' as well. Delivery reports include the batch ID, recipient number, and delivery status.

```
// app/api/sinch/incoming/route.ts
import { NextResponse } from 'next/server';

interface SinchInboundMessage {
  type: 'mo_text' | 'mo_binary';
  from: string;
  to: string;
  body: string;
  id: string;
  received_at: string;
}

export async function POST(request: Request) {
  try {
    const message = await request.json() as SinchInboundMessage;

    console.log(`Inbound SMS from ${message.from}: ${message.body}`);
    // Save to database:
    // await supabase.from('messages').insert({
    //   from_number: message.from,
    //   body: message.body,
    //   sinch_id: message.id,
    //   received_at: message.received_at,
    // });

    // Return 200 quickly — Sinch retries if it doesn't receive 200
    return NextResponse.json({ status: 'ok' });
  } catch (error: unknown) {
    const e = error as { message: string };
    console.error('Sinch webhook error:', e.message);
    return NextResponse.json({ status: 'ok' }); // Return 200 even on errors to prevent retries
  }
}
```

**Expected result:** After deploying and configuring the callback URL in the Sinch dashboard, inbound SMS messages to your Sinch number trigger the webhook handler. The message is logged and saved to your database.

## Best practices

- Store SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, and verification credentials as server-side environment variables only — never use NEXT_PUBLIC_ prefix for communication API credentials
- Use Sinch's batch API for bulk sending rather than individual API calls in a loop — a single batch call can send to 1,000 recipients and is dramatically more efficient
- Always return HTTP 200 from Sinch webhook handlers before processing — Sinch retries webhooks that don't get a quick 200 response, potentially causing duplicate message delivery
- Validate phone numbers to E.164 format before sending to Sinch — invalid formats cause API errors and wasted credits
- Use Sinch Verification for OTP instead of building custom code generation — it handles expiry, rate limiting, and fraud prevention automatically
- Store Sinch batch IDs after sending so you can later check delivery reports for each batch — important for debugging undelivered notifications
- For RCS messages, always implement SMS fallback — only about 60% of Android users have RCS enabled, and iOS users don't have it at all

## Use cases

### Transactional SMS Notifications

Send order confirmations, shipping updates, appointment reminders, and security alerts to users via SMS. Sinch's competitive global pricing makes it cost-effective for high-volume notifications. The API route accepts recipient numbers and messages, validates input, and sends via Sinch's Messages API.

Prompt example:

```
Create a Sinch SMS notification system. Create an API route at /api/sinch/send-sms (POST) that accepts 'to' (array of E.164 phone numbers, max 10) and 'message' (string). Use HTTP Basic auth with SINCH_SERVICE_PLAN_ID and SINCH_API_TOKEN from env. POST to https://us.sms.api.sinch.com/xms/v1/{servicePlanId}/batches with the recipients and message body. Return the batch ID. Add a notification preferences form where users enter their phone number to receive order updates.
```

### RCS Rich Messaging for Android Users

Send RCS messages with images, buttons, and branded sender identity to Android users. RCS messages look like native app messages with your company logo and colors. Fall back to SMS automatically for users whose devices don't support RCS. Build an order update flow that sends rich confirmation messages with tracking links as buttons.

Prompt example:

```
Build an RCS order confirmation message. Create an API route at /api/sinch/send-rcs (POST) that accepts orderId, customerPhone, trackingUrl, and estimatedDelivery. Use the Sinch RCS API to send a rich message with: branded header (store logo), order summary text, and a 'Track Package' button that opens the trackingUrl. Fall back to a plain SMS if RCS is not available. Use SINCH_PROJECT_ID and SINCH_KEY_ID and SINCH_KEY_SECRET for RCS auth. Display send confirmation and message ID on success.
```

### Phone Number Verification OTP

Verify user phone numbers during signup using Sinch Verification. Users enter their phone number, receive a 6-digit PIN via SMS, and enter it to complete verification. Sinch handles code generation, expiry, rate limiting, and multi-channel fallback (SMS → voice). Simpler than building OTP logic yourself.

Prompt example:

```
Implement phone verification with Sinch Verification API. Create two API routes: /api/sinch/verify/start (POST) that accepts phoneNumber in E.164 format and initiates a Sinch SMS verification by calling POST https://verificationapi-v1.sinch.com/verification/v1/verifications/number with method:sms. Create /api/sinch/verify/check (POST) that accepts phoneNumber and code, then calls PUT https://verificationapi-v1.sinch.com/verification/v1/verifications/number/{phoneNumber} to verify. Use SINCH_VERIFICATION_APP_KEY and SINCH_VERIFICATION_APP_SECRET from env. Build a two-step verification UI.
```

## Troubleshooting

### API returns 401 Unauthorized or 'Authentication failed'

Cause: SINCH_SERVICE_PLAN_ID or SINCH_API_TOKEN is incorrect. The Basic auth username must be the Service Plan ID (UUID format), not your Sinch account email. The password is the API Token, not your account password.

Solution: Go to dashboard.sinch.com → SMS → Service Plans → click your service plan. Copy the 'Service Plan ID' (looks like a UUID: 'a5a17e88-xxxx-xxxx-xxxx-xxxxxxxxxxxx') and the 'API Token'. These are different from your Sinch login credentials. Verify no whitespace was copied around the values in your .env file.

```
// Debug: log partial credentials to verify (never log full credentials):
console.log('Service Plan ID format:', process.env.SINCH_SERVICE_PLAN_ID?.slice(0, 8) + '...');
// Should look like: a5a17e88-...
```

### Message sends but never arrives at destination

Cause: The sender number may not be activated for SMS in the recipient's country, or the recipient number format is incorrect. Sinch shared numbers have geographic restrictions.

Solution: Verify the destination number is in E.164 format (+1XXXXXXXXXX for US). Check your Sinch dashboard's message logs for delivery status and error codes. For international SMS, ensure your service plan has international messaging enabled and the sender number supports that destination country.

### Sinch Verification returns 'FAILED' instead of 'SUCCESSFUL'

Cause: The code entered is incorrect, the verification has expired (typically 5 minutes), or too many failed attempts have triggered rate limiting.

Solution: Sinch Verification codes expire after 5 minutes by default. If the user took too long, they need to request a new code. Implement a resend flow in your UI. After 5 failed attempts, the verification is blocked for the hour — inform users with a clear error message rather than a generic failure.

```
// Check specific failure reason in the response:
if (result.status === 'FAILED') {
  // Check result.reason for: 'EXPIRED', 'TOO_MANY_ATTEMPTS', 'WRONG_CODE'
  const reason = result.reason || 'UNKNOWN';
  return NextResponse.json(
    { verified: false, reason },
    { status: 400 }
  );
}
```

### Inbound SMS webhooks not received during Bolt development

Cause: This is expected. Sinch inbound message callbacks are outbound HTTP POST requests from Sinch's infrastructure to your endpoint. Bolt's WebContainer runs inside a browser tab with no public URL.

Solution: Deploy to Netlify or Bolt Cloud first. After deployment, configure the 'Incoming Message Callback URL' in your Sinch service plan settings to point to your deployed API endpoint. Use Sinch's dashboard message logs to verify that inbound messages are received and the callback is being attempted.

## Frequently asked questions

### Does Sinch have an npm package that works in Bolt's WebContainer?

Yes. The @sinch/sdk-core npm package is pure JavaScript and works in Bolt's WebContainer. Alternatively, you can use the raw REST API with fetch(), which also works perfectly. Both approaches communicate over HTTPS and have no native binary dependencies. The raw fetch approach is simpler to set up; the SDK provides TypeScript types and method signatures for each API operation.

### What makes Sinch different from Twilio for SMS?

Sinch's primary differentiators are RCS support (rich messaging on Android with images and buttons), competitive global pricing especially in Asian and Latin American markets, and its email capabilities through Mailgun and SparkPost acquisitions. Twilio has a larger developer community, more extensive documentation, and the Twilio Verify service for managed OTP. For pure SMS volume in regions where Sinch has strong carrier relationships, Sinch can be 20-40% cheaper than Twilio.

### What is RCS and does it work with all phones?

RCS (Rich Communication Services) is an upgraded messaging standard that enables branded sender identity, images, carousels, quick reply buttons, and read receipts — similar to iMessage but for Android's native messaging app. RCS works on Android devices with Google Messages and an RCS-enabled carrier. It does NOT work on iOS (Apple uses iMessage instead). For RCS messages, Sinch automatically falls back to standard SMS when the recipient's device doesn't support RCS, so you always need SMS fallback content.

### Can I send MMS (picture messages) with Sinch?

Yes. Sinch supports MMS in the US and Canada. To send an MMS, use the same batch API endpoint but include a media_urls array in the request body alongside the text body. Media URLs must be publicly accessible HTTPS URLs. MMS pricing is higher than SMS — typically $0.01-0.03 per message compared to $0.003-0.008 for SMS. US-only carriers impose size limits (typically 600KB for MMS attachments).

### How do I test Sinch SMS without spending credits?

Sinch trial accounts include free credits sufficient for testing. Sinch does not have a separate sandbox with fake numbers like Twilio's test credentials — your test messages use real credits but the amounts are small ($0.003-0.008 per message). Create a test phone number in your Sinch service plan and send test messages to your own verified mobile number. Check the message logs in the Sinch dashboard to verify delivery without waiting for the physical message.

---

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