SMS from a Flutter App (Dart Backend)
Send SMS from a Flutter app using a Dart backend server — keeps your API key secure on the server
SMS from a Flutter App (Dart Backend)
Flutter apps run on user devices, so you must never bundle your CastBrick API key in the app. This tutorial builds a lightweight Dart backend (using shelf) that your Flutter app calls to send SMS — the API key stays on the server.
Flutter App → POST /sms/send → Dart Server → CastBrick APIWhat you'll build
- A Dart
shelfHTTP server that exposes a/sms/sendendpoint - A Flutter widget with a button that triggers an SMS via the backend
Prerequisites
- Dart 3+ / Flutter 3.10+
- A CastBrick API key from the dashboard
Part 1: The Dart backend
mkdir castbrick-backend && cd castbrick-backend
dart create -t server-shelf .
dart pub add castbrickReplace bin/server.dart with:
import 'dart:convert';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:castbrick/castbrick.dart';
void main() async {
final apiKey = Platform.environment['CASTBRICK_API_KEY'];
if (apiKey == null || apiKey.isEmpty) {
stderr.writeln('CASTBRICK_API_KEY is not set');
exit(1);
}
final cb = CastBrick(apiKey: apiKey);
final router = Router();
router.post('/sms/send', (Request request) async {
final body = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
final phone = body['phone'] as String?;
final message = body['message'] as String?;
if (phone == null || phone.isEmpty) {
return Response(422, body: jsonEncode({'error': 'phone is required'}));
}
if (message == null || message.isEmpty) {
return Response(422, body: jsonEncode({'error': 'message is required'}));
}
try {
final result = await cb.sms.send(
to: [phone],
content: message,
senderId: 'MyApp',
);
return Response.ok(
jsonEncode({'ok': true, 'messageId': result.messageId}),
headers: {'Content-Type': 'application/json'},
);
} on CastBrickApiError catch (e) {
return Response(
e.status,
body: jsonEncode({'error': e.body}),
headers: {'Content-Type': 'application/json'},
);
} finally {
cb.close();
}
});
final handler = const Pipeline()
.addMiddleware(logRequests())
.addHandler(router.call);
final server = await io.serve(handler, '0.0.0.0', 8080);
print('Server running at http://${server.address.host}:${server.port}');
}Add shelf_router to pubspec.yaml:
dart pub add shelf_routerRun the server:
export CASTBRICK_API_KEY=your_api_key_here
dart run bin/server.dartTest it:
curl -X POST http://localhost:8080/sms/send \
-H "Content-Type: application/json" \
-d '{"phone": "+244923000000", "message": "Hello from Dart backend!"}'Part 2: The Flutter app
In your Flutter project, add a simple service that calls your backend:
// lib/services/sms_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class SmsService {
final String _baseUrl;
SmsService({String baseUrl = 'http://localhost:8080'}) : _baseUrl = baseUrl;
Future<void> sendSms({
required String phone,
required String message,
}) async {
final response = await http.post(
Uri.parse('$_baseUrl/sms/send'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'phone': phone, 'message': message}),
);
if (response.statusCode != 200) {
final error = jsonDecode(response.body)['error'] ?? 'Unknown error';
throw Exception('SMS failed: $error');
}
}
}Use it in a widget:
// lib/screens/verification_screen.dart
import 'package:flutter/material.dart';
import '../services/sms_service.dart';
class VerificationScreen extends StatefulWidget {
const VerificationScreen({super.key});
@override
State<VerificationScreen> createState() => _VerificationScreenState();
}
class _VerificationScreenState extends State<VerificationScreen> {
final _smsService = SmsService();
final _phoneController = TextEditingController();
bool _sending = false;
Future<void> _sendVerification() async {
final phone = _phoneController.text.trim();
if (phone.isEmpty) return;
setState(() => _sending = true);
try {
await _smsService.sendSms(
phone: phone,
message: 'Your verification code is 123456. Expires in 10 minutes.',
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Code sent!')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
} finally {
if (mounted) setState(() => _sending = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Phone Verification')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _phoneController,
decoration: const InputDecoration(
labelText: 'Phone number',
hintText: '+244923000000',
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _sending ? null : _sendVerification,
child: _sending
? const CircularProgressIndicator()
: const Text('Send verification code'),
),
],
),
),
);
}
}In production, replace http://localhost:8080 with your deployed backend URL, and add authentication between Flutter and your backend (e.g., a shared secret header or Firebase Auth token) so only your app can call the SMS endpoint.
Deploying the backend
Deploy the Dart server to any platform that runs Docker:
# Dockerfile
FROM dart:stable AS build
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o bin/server
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=build /app/bin/server ./server
EXPOSE 8080
CMD ["./server"]docker build -t castbrick-backend .
docker run -p 8080:8080 -e CASTBRICK_API_KEY=your_key castbrick-backendNext steps
- Dart SDK reference — contacts, broadcasts, scheduling
- Node.js OTP tutorial — if you prefer a Node.js backend
- Webhooks — track delivery status from your backend