Flutter App Integration Guide

Connect your Convergence Protocol Flutter app to the audiobook distribution platform for seamless sync between web and mobile.

๐ŸŽฏ What You Get

FeatureWebFlutter AppSync
User Accountsโœ…โœ…โœ…
Convergence Progressโœ…โœ…โœ…
Session Loggingโœ…โœ…โœ…
77Hz Audioโœ…โœ…โœ…
Audiobook Streamingโœ…โœ…โœ…
Offline ModeโŒโœ…Sync when online

๐Ÿ”— API Endpoints for Flutter

Base URL

https://audio.kbird.ai

Authentication

Same as web - use JWT tokens:

// Login
POST /auth/login
Body: { "email": "[email protected]", "password": "password" }
Response: { "token": "eyJhbG...", "user": {...} }
 
// Store token securely (flutter_secure_storage)
// Include in all requests: headers['Authorization'] = 'Bearer $token'

Convergence Protocol Endpoints

Get Progress

GET /api/convergence/progress
Headers: { 'Authorization': 'Bearer $token' }
 
Response: {
  "progress": {
    "current_node": 5,
    "total_sessions": 12,
    "streak_days": 3,
    "longest_streak": 7,
    "last_session_date": "2026-03-16"
  },
  "completedNodes": [1, 2, 3, 4]
}

Update Progress

POST /api/convergence/progress
Headers: { 'Authorization': 'Bearer $token', 'Content-Type': 'application/json' }
Body: {
  "currentNode": 5,
  "streakDays": 3,
  "longestStreak": 7,
  "lastSessionDate": "2026-03-16"
}

Log Session

POST /api/convergence/sessions
Body: {
  "sessionType": "node_practice", // or "77hz", "weekly_convergence", "audio_listening"
  "nodeNumber": 5,
  "durationMinutes": 15,
  "audioFile": "wildflower_ch18.mp3",
  "book": "wildflower",
  "notes": "Felt the pattern breaking today",
  "moodBefore": 6,
  "moodAfter": 8,
  "syncTimestamp": 1710595200000 // Unix timestamp for sync
}

Get Sessions

GET /api/convergence/sessions?limit=50&offset=0
Response: {
  "sessions": [
    {
      "id": 1,
      "session_type": "77hz",
      "duration_minutes": 20,
      "created_at": "2026-03-16T09:30:00Z"
    }
  ]
}

Complete a Node

POST /api/convergence/nodes/complete
Body: { "nodeNumber": 5 }

Weekly Convergence

// Get current week
GET /api/convergence/weekly
 
// Save weekly check-in
POST /api/convergence/weekly
Body: {
  "weekStartDate": "2026-03-10",
  "weekNumber": 3,
  "score": 75,
  "patternRecognition": "Noticed resistance to silence",
  "resistanceEncountered": "Skipped Tuesday practice",
  "insightGained": "Silence is where patterns reveal themselves",
  "nextWeekFocus": "Daily 10-minute silence",
  "completed": true
}

77Hz Entrainment

Start Session

POST /api/convergence/entrainment/start
Body: {
  "sessionId": "28409296", // or custom
  "frequencyHz": 77,
  "type": "session_28409296"
}
Response: { "entrainmentId": 123, "message": "77Hz session started" }

Stream 77Hz Audio

// Stream for playback
GET /audio/77hz/77Hz_Entrainment_Base.mp3
Headers: { 'Authorization': 'Bearer $token' }
 
// For Flutter: use just_audio package with headers

Complete Session

POST /api/convergence/entrainment/complete
Body: {
  "entrainmentId": 123,
  "durationMinutes": 20
}

Device Sync

POST /api/convergence/sync
Body: {
  "deviceId": "unique-device-identifier",
  "deviceType": "ios", // or "android"
  "lastSyncTimestamp": 1710595200000 // last sync time
}
 
Response: {
  "syncTimestamp": 1710595201000,
  "sessions": [...], // sessions since last sync
  "progress": {...} // current progress
}

๐Ÿ“ฑ Flutter Implementation Example

Dependencies

# pubspec.yaml
dependencies:
  http: ^1.2.0
  flutter_secure_storage: ^9.0.0
  just_audio: ^0.9.36
  audio_session: ^0.1.18
  shared_preferences: ^2.2.2

API Service

class ConvergenceApiService {
  static const String baseUrl = 'https://audio.kbird.ai';
  final FlutterSecureStorage _storage = const FlutterSecureStorage();
  
  Future<String?> getToken() async {
    return await _storage.read(key: 'access_token');
  }
  
  Future<Map<String, String>> getHeaders() async {
    final token = await getToken();
    return {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    };
  }
  
  // Get convergence progress
  Future<ConvergenceProgress> getProgress() async {
    final response = await http.get(
      Uri.parse('$baseUrl/api/convergence/progress'),
      headers: await getHeaders(),
    );
    
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      return ConvergenceProgress.fromJson(data['progress']);
    }
    throw Exception('Failed to get progress');
  }
  
  // Log a session
  Future<void> logSession(Session session) async {
    final response = await http.post(
      Uri.parse('$baseUrl/api/convergence/sessions'),
      headers: await getHeaders(),
      body: jsonEncode(session.toJson()),
    );
    
    if (response.statusCode != 200) {
      // Store locally for later sync
      await _storeOffline(session);
    }
  }
  
  // Sync with server
  Future<void> sync() async {
    final lastSync = await _getLastSyncTimestamp();
    
    // Get server changes
    final response = await http.post(
      Uri.parse('$baseUrl/api/convergence/sync'),
      headers: await getHeaders(),
      body: jsonEncode({
        'deviceId': await _getDeviceId(),
        'deviceType': Platform.isIOS ? 'ios' : 'android',
        'lastSyncTimestamp': lastSync,
      }),
    );
    
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      
      // Apply server changes
      await _applyServerChanges(data);
      
      // Send local changes
      await _pushLocalChanges();
      
      // Update last sync
      await _setLastSyncTimestamp(data['syncTimestamp']);
    }
  }
  
  // Stream 77Hz audio
  Future<AudioSource> get77HzAudio(String filename) async {
    final token = await getToken();
    return LockCachingAudioSource(
      Uri.parse('$baseUrl/audio/77hz/$filename'),
      headers: {'Authorization': 'Bearer $token'},
    );
  }
  
  // Stream audiobook (premium content)
  Future<AudioSource> getAudiobook(String book, String format, String filename) async {
    final token = await getToken();
    return ProgressiveAudioSource(
      Uri.parse('$baseUrl/audio/$book/$format/$filename'),
      headers: {'Authorization': 'Bearer $token'},
    );
  }
}

Data Models

class ConvergenceProgress {
  final int currentNode;
  final int totalSessions;
  final int streakDays;
  final int longestStreak;
  final DateTime? lastSessionDate;
  
  ConvergenceProgress({
    required this.currentNode,
    required this.totalSessions,
    required this.streakDays,
    required this.longestStreak,
    this.lastSessionDate,
  });
  
  factory ConvergenceProgress.fromJson(Map<String, dynamic> json) {
    return ConvergenceProgress(
      currentNode: json['current_node'] ?? 1,
      totalSessions: json['total_sessions'] ?? 0,
      streakDays: json['streak_days'] ?? 0,
      longestStreak: json['longest_streak'] ?? 0,
      lastSessionDate: json['last_session_date'] != null 
        ? DateTime.parse(json['last_session_date'])
        : null,
    );
  }
  
  Map<String, dynamic> toJson() => {
    'currentNode': currentNode,
    'streakDays': streakDays,
    'longestStreak': longestStreak,
    'lastSessionDate': lastSessionDate?.toIso8601String().split('T')[0],
  };
}
 
class Session {
  final String sessionType;
  final int? nodeNumber;
  final int durationMinutes;
  final String? audioFile;
  final String? book;
  final String? notes;
  final int? moodBefore;
  final int? moodAfter;
  final int syncTimestamp;
  
  Session({
    required this.sessionType,
    this.nodeNumber,
    this.durationMinutes = 0,
    this.audioFile,
    this.book,
    this.notes,
    this.moodBefore,
    this.moodAfter,
  }) : syncTimestamp = DateTime.now().millisecondsSinceEpoch;
  
  Map<String, dynamic> toJson() => {
    'sessionType': sessionType,
    'nodeNumber': nodeNumber,
    'durationMinutes': durationMinutes,
    'audioFile': audioFile,
    'book': book,
    'notes': notes,
    'moodBefore': moodBefore,
    'moodAfter': moodAfter,
    'syncTimestamp': syncTimestamp,
  };
}

Offline Support

class OfflineQueue {
  static const String _key = 'offline_sessions';
  
  Future<void> add(Session session) async {
    final prefs = await SharedPreferences.getInstance();
    final List<String> queue = prefs.getStringList(_key) ?? [];
    queue.add(jsonEncode(session.toJson()));
    await prefs.setStringList(_key, queue);
  }
  
  Future<List<Session>> getAll() async {
    final prefs = await SharedPreferences.getInstance();
    final List<String> queue = prefs.getStringList(_key) ?? [];
    return queue
      .map((s) => Session.fromJson(jsonDecode(s)))
      .toList();
  }
  
  Future<void> clear() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_key);
  }
  
  Future<void> sync() async {
    final sessions = await getAll();
    for (final session in sessions) {
      try {
        await apiService.logSession(session);
      } catch (e) {
        // Keep in queue if fails
        return;
      }
    }
    await clear();
  }
}

๐Ÿ”„ Sync Strategy

Automatic Sync

  • On app launch
  • After logging a session
  • When returning online
  • Every 5 minutes during active use

Conflict Resolution

  • Server wins for progress (higher node number)
  • Append sessions (both kept)
  • Latest weekly convergence wins

Offline Mode

// Check connectivity
if (await Connectivity().checkConnectivity() == ConnectivityResult.none) {
  // Store locally
  await offlineQueue.add(session);
} else {
  // Send to server
  await apiService.logSession(session);
}
 
// When coming back online
Connectivity().onConnectivityChanged.listen((result) {
  if (result != ConnectivityResult.none) {
    offlineQueue.sync();
  }
});

๐ŸŽต 77Hz Audio Player

class EntrainmentPlayer {
  final AudioPlayer _player = AudioPlayer();
  final ConvergenceApiService _api = ConvergenceApiService();
  int? _currentEntrainmentId;
  
  Future<void> start77HzSession() async {
    // Start session on server
    final response = await _api.startEntrainment();
    _currentEntrainmentId = response['entrainmentId'];
    
    // Load and play audio
    final audioSource = await _api.get77HzAudio('77Hz_Entrainment_Base.mp3');
    await _player.setAudioSource(audioSource);
    await _player.play();
  }
  
  Future<void> stopSession() async {
    final duration = _player.duration?.inMinutes ?? 0;
    await _player.stop();
    
    // Complete on server
    if (_currentEntrainmentId != null) {
      await _api.completeEntrainment(_currentEntrainmentId!, duration);
    }
  }
}

๐Ÿ“Š Tracking Convergence Protocol

// When user completes a node practice
await apiService.logSession(Session(
  sessionType: 'node_practice',
  nodeNumber: currentNode,
  durationMinutes: practiceDuration,
  notes: userNotes,
  moodBefore: moodBefore,
  moodAfter: moodAfter,
));
 
// Mark node complete
await apiService.completeNode(currentNode);
 
// Sync progress
await apiService.sync();

๐Ÿ” Security

  • Store JWT in flutter_secure_storage
  • Refresh token automatically when expired
  • Clear tokens on logout
  • Use HTTPS only

๐Ÿงช Testing

// Test sync
test('Sync merges local and server data', () async {
  // Create local session
  await offlineQueue.add(Session(sessionType: 'test'));
  
  // Sync
  await apiService.sync();
  
  // Verify on server
  final sessions = await apiService.getSessions();
  expect(sessions.any((s) => s.sessionType == 'test'), true);
});

๐Ÿ“š Resources


Questions? The web and Flutter apps share the same API - any changes sync across both platforms automatically!