CastBricks Docs

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 API

What you'll build

  • A Dart shelf HTTP server that exposes a /sms/send endpoint
  • 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 castbrick

Replace 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_router

Run the server:

export CASTBRICK_API_KEY=your_api_key_here
dart run bin/server.dart

Test 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-backend

Next steps