Skip to Content
User GuidesMobile App Integration Guide

Mobile App Integration Guide

Collect user feedback directly from your mobile app using the Miniback API. No widget required — just simple HTTP requests.

⚡ Quick Start (5 minutes)

  1. Create a project in your Miniback dashboard
  2. Generate an API key: Project Settings → API Access → Generate Key
  3. POST feedback to: https://miniback.io/api/projects/{projectId}/feedback
  4. Include x-api-key header with your API key

All plans supported — FREE plan includes 300 API calls/month

Overview

Unlike web integrations that use the embeddable widget, mobile apps submit feedback programmatically via the REST API. This gives you full control over:

  • When to collect feedback (after key actions, on demand, etc.)
  • What context to include (device info, app version, user ID, etc.)
  • How to present the feedback UI (your own design, native components)

Prerequisites

  • A Miniback account (sign up here )
  • At least one project created
  • An API key for your project

Step 1: Get Your API Key

  1. Sign in to your Miniback dashboard 
  2. Navigate to your project
  3. Scroll to the API Access section
  4. Click “Generate API Key” if you don’t have one
  5. Copy your API key (starts with mbk_)

⚠️ Security: Store your API key securely. Never hardcode it in client-side code that gets shipped to users. Use secure storage or environment configuration.

Step 2: Submit Feedback

API Endpoint

POST /api/projects/{projectId}/feedback

Request Headers

HeaderValueRequired
x-api-keyYour API key (mbk_...)✅ Yes
Content-Typeapplication/json✅ Yes

Request Body

{ "message": "User's feedback message", "context": { "url": "myapp://checkout", "userAgent": "MyApp/2.1.0 (iOS 17.2; iPhone 15 Pro)", "timestamp": "2025-01-15T10:30:00.000Z", "screen": { "width": 1179, "height": 2556 }, "appVersion": "2.1.0", "platform": "iOS", "osVersion": "17.2", "deviceModel": "iPhone 15 Pro", "userId": "user_123", "screenName": "CheckoutScreen" } }
FieldTypeRequiredDescription
messagestring✅ YesThe feedback message from the user
contextobject❌ NoAdditional metadata about the feedback

Response

Success (201 Created):

{ "data": { "id": "feedback_abc123", "message": "User's feedback message", "context": { ... }, "status": "NEW", "createdAt": "2025-01-15T10:30:00.000Z" } }

Error (4xx/5xx):

{ "error": "Error message describing the issue" }

Code Examples

The following examples demonstrate how to build the context object with the correct field structure. Use these as templates — the buildContext() methods are designed to produce the exact field names and types expected by Miniback.

iOS (Swift)

import Foundation struct FeedbackService { private let projectId: String private let apiKey: String private let baseURL = "https://miniback.io" init(projectId: String, apiKey: String) { self.projectId = projectId self.apiKey = apiKey } func submitFeedback( message: String, context: [String: Any]? = nil ) async throws -> String { let url = URL(string: "\(baseURL)/api/projects/\(projectId)/feedback")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(apiKey, forHTTPHeaderField: "x-api-key") var body: [String: Any] = ["message": message] if let context = context { body["context"] = context } request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw FeedbackError.submissionFailed } let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let feedbackData = json?["data"] as? [String: Any] return feedbackData?["id"] as? String ?? "" } func buildContext(screenName: String? = nil) -> [String: Any] { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" let osVersion = UIDevice.current.systemVersion let deviceModel = UIDevice.current.model return [ // Standard fields (widget-compatible) "url": screenName ?? "unknown", "userAgent": "MyApp/\(appVersion) (iOS \(osVersion); \(deviceModel))", "timestamp": ISO8601DateFormatter().string(from: Date()), "screen": [ "width": Int(UIScreen.main.bounds.width * UIScreen.main.scale), "height": Int(UIScreen.main.bounds.height * UIScreen.main.scale) ], // Mobile-specific fields "platform": "iOS", "appVersion": appVersion, "osVersion": osVersion, "deviceModel": deviceModel, "screenName": screenName ?? "unknown" ] } } enum FeedbackError: Error { case submissionFailed } // Usage let feedbackService = FeedbackService( projectId: "your_project_id", apiKey: "mbk_your_api_key" ) Task { do { let feedbackId = try await feedbackService.submitFeedback( message: "The checkout button is hard to find", context: feedbackService.buildContext(screenName: "CheckoutScreen") ) print("Feedback submitted: \(feedbackId)") } catch { print("Failed to submit feedback: \(error)") } }

Android (Kotlin)

import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL class FeedbackService( private val projectId: String, private val apiKey: String ) { private val baseUrl = "https://miniback.io" suspend fun submitFeedback( message: String, context: Map<String, Any>? = null ): Result<String> = withContext(Dispatchers.IO) { try { val url = URL("$baseUrl/api/projects/$projectId/feedback") val connection = url.openConnection() as HttpURLConnection connection.apply { requestMethod = "POST" setRequestProperty("Content-Type", "application/json") setRequestProperty("x-api-key", apiKey) doOutput = true } val body = JSONObject().apply { put("message", message) context?.let { put("context", JSONObject(it)) } } connection.outputStream.use { it.write(body.toString().toByteArray()) } if (connection.responseCode in 200..299) { val response = connection.inputStream.bufferedReader().readText() val json = JSONObject(response) val feedbackId = json.getJSONObject("data").getString("id") Result.success(feedbackId) } else { Result.failure(Exception("Submission failed: ${connection.responseCode}")) } } catch (e: Exception) { Result.failure(e) } } fun buildContext(context: android.content.Context, screenName: String? = null): Map<String, Any> { val appVersion = context.packageManager .getPackageInfo(context.packageName, 0).versionName ?: "unknown" val osVersion = android.os.Build.VERSION.RELEASE val deviceModel = android.os.Build.MODEL val displayMetrics = context.resources.displayMetrics return mapOf( // Standard fields (widget-compatible) "url" to (screenName ?: "unknown"), "userAgent" to "MyApp/$appVersion (Android $osVersion; $deviceModel)", "timestamp" to java.time.Instant.now().toString(), "screen" to mapOf( "width" to displayMetrics.widthPixels, "height" to displayMetrics.heightPixels ), // Mobile-specific fields "platform" to "Android", "appVersion" to appVersion, "osVersion" to osVersion, "deviceModel" to deviceModel, "screenName" to (screenName ?: "unknown") ) } } // Usage val feedbackService = FeedbackService( projectId = "your_project_id", apiKey = "mbk_your_api_key" ) lifecycleScope.launch { feedbackService.submitFeedback( message = "App crashes when I tap the menu", context = feedbackService.buildContext(applicationContext, "MenuScreen") ).onSuccess { feedbackId -> Log.d("Feedback", "Submitted: $feedbackId") }.onFailure { error -> Log.e("Feedback", "Failed: ${error.message}") } }

React Native

interface FeedbackContext { // Standard fields (widget-compatible) url?: string; userAgent?: string; timestamp?: string; screen?: { width: number; height: number }; // Mobile-specific fields platform?: string; appVersion?: string; osVersion?: string; deviceModel?: string; screenName?: string; userId?: string; [key: string]: any; } interface FeedbackResponse { data: { id: string; message: string; status: string; createdAt: string; }; } class FeedbackService { private projectId: string; private apiKey: string; private baseUrl = 'https://miniback.io'; constructor(projectId: string, apiKey: string) { this.projectId = projectId; this.apiKey = apiKey; } async submitFeedback( message: string, context?: FeedbackContext ): Promise<string> { const response = await fetch( `${this.baseUrl}/api/projects/${this.projectId}/feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey, }, body: JSON.stringify({ message, context }), } ); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to submit feedback'); } const data: FeedbackResponse = await response.json(); return data.data.id; } buildContext(screenName?: string): FeedbackContext { const { Platform, Dimensions } = require('react-native'); const DeviceInfo = require('react-native-device-info'); const appVersion = DeviceInfo.getVersion(); const osVersion = Platform.Version.toString(); const deviceModel = DeviceInfo.getModel(); const { width, height } = Dimensions.get('screen'); return { // Standard fields (widget-compatible) url: screenName || 'unknown', userAgent: `MyApp/${appVersion} (${Platform.OS} ${osVersion}; ${deviceModel})`, timestamp: new Date().toISOString(), screen: { width, height }, // Mobile-specific fields platform: Platform.OS, appVersion, osVersion, deviceModel, screenName: screenName || 'unknown', }; } } // Usage const feedbackService = new FeedbackService( 'your_project_id', 'mbk_your_api_key' ); async function sendFeedback(userMessage: string, screenName: string) { try { const feedbackId = await feedbackService.submitFeedback( userMessage, feedbackService.buildContext(screenName) ); console.log('Feedback submitted:', feedbackId); } catch (error) { console.error('Failed to submit feedback:', error); } } // Example: sendFeedback("Bug on checkout", "CheckoutScreen");

Flutter (Dart)

import 'dart:convert'; import 'dart:io'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; class FeedbackService { final String projectId; final String apiKey; final String baseUrl = 'https://miniback.io'; FeedbackService({ required this.projectId, required this.apiKey, }); Future<String> submitFeedback( String message, { Map<String, dynamic>? context, }) async { final url = Uri.parse('$baseUrl/api/projects/$projectId/feedback'); final response = await http.post( url, headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, }, body: jsonEncode({ 'message': message, if (context != null) 'context': context, }), ); if (response.statusCode >= 200 && response.statusCode < 300) { final data = jsonDecode(response.body); return data['data']['id']; } else { final error = jsonDecode(response.body); throw Exception(error['error'] ?? 'Failed to submit feedback'); } } Future<Map<String, dynamic>> buildContext({String? screenName}) async { final deviceInfo = DeviceInfoPlugin(); final packageInfo = await PackageInfo.fromPlatform(); String platform = 'unknown'; String osVersion = 'unknown'; String deviceModel = 'unknown'; if (Platform.isAndroid) { final androidInfo = await deviceInfo.androidInfo; platform = 'Android'; osVersion = androidInfo.version.release; deviceModel = androidInfo.model; } else if (Platform.isIOS) { final iosInfo = await deviceInfo.iosInfo; platform = 'iOS'; osVersion = iosInfo.systemVersion; deviceModel = iosInfo.model; } // Get screen dimensions final window = WidgetsBinding.instance.window; final screenWidth = window.physicalSize.width.toInt(); final screenHeight = window.physicalSize.height.toInt(); return { // Standard fields (widget-compatible) 'url': screenName ?? 'unknown', 'userAgent': '${packageInfo.appName}/${packageInfo.version} ($platform $osVersion; $deviceModel)', 'timestamp': DateTime.now().toIso8601String(), 'screen': { 'width': screenWidth, 'height': screenHeight, }, // Mobile-specific fields 'platform': platform, 'appVersion': packageInfo.version, 'osVersion': osVersion, 'deviceModel': deviceModel, 'screenName': screenName ?? 'unknown', 'buildNumber': packageInfo.buildNumber, }; } } // Usage final feedbackService = FeedbackService( projectId: 'your_project_id', apiKey: 'mbk_your_api_key', ); Future<void> sendFeedback(String userMessage, String screenName) async { try { final context = await feedbackService.buildContext(screenName: screenName); final feedbackId = await feedbackService.submitFeedback( userMessage, context: context, ); print('Feedback submitted: $feedbackId'); } catch (e) { print('Failed to submit feedback: $e'); } } // Example: sendFeedback("Bug on checkout", "CheckoutScreen");

⚠️ Important: Follow the Field Structure Carefully

To ensure your mobile feedback integrates seamlessly with the dashboard analytics and can be analyzed alongside web widget feedback, you must follow the exact field names and structure documented below. Inconsistent field names will result in fragmented data that cannot be properly filtered, searched, or aggregated.

To ensure consistency with the web widget and enable unified analytics, use these standard field names.

Standard Fields (Widget-Compatible)

These fields match the web widget format for consistent analytics across platforms:

FieldTypeDescriptionExample
urlstringDeep link or screen identifier"myapp://checkout", "CheckoutScreen"
userAgentstringApp and device identifier string"MyApp/2.1.0 (iOS 17.2; iPhone 15 Pro)"
timestampstringISO 8601 timestamp"2025-01-15T10:30:00.000Z"
screenobjectDevice screen dimensions{ "width": 1179, "height": 2556 }

Mobile-Specific Fields

Additional fields useful for mobile debugging and analytics:

FieldTypeDescriptionExample
platformstringOperating system"iOS", "Android"
appVersionstringYour app version"2.1.0"
osVersionstringOS version"17.2", "14"
deviceModelstringDevice model"iPhone 15 Pro", "Pixel 8"
screenNamestringCurrent screen/view name"CheckoutScreen"
userIdstringYour internal user ID"user_abc123"
userEmailstringUser’s email (if consented)"user@example.com"
sessionIdstringSession identifier"sess_xyz789"
localestringUser’s language/region"en-US"
buildNumberstringApp build number"142"

Tip: The userAgent field is particularly useful for filtering feedback by platform in the dashboard. Format it as: AppName/Version (OS Version; Device Model)

Best Practices

1. Secure Your API Key

// ❌ DON'T hardcode in source let apiKey = "mbk_abc123..." // ✅ DO use secure storage or config let apiKey = ProcessInfo.processInfo.environment["MINIBACK_API_KEY"] // Or use Keychain (iOS), EncryptedSharedPreferences (Android), etc.

2. Handle Errors Gracefully

async function submitWithRetry(message: string, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await feedbackService.submitFeedback(message); } catch (error) { if (attempt === maxRetries) throw error; await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff } } }

3. Queue Feedback When Offline

// Store feedback locally when offline async function queueFeedback(message: string, context: FeedbackContext) { const isOnline = await NetInfo.fetch().then(state => state.isConnected); if (isOnline) { await feedbackService.submitFeedback(message, context); } else { // Store in local queue (AsyncStorage, SQLite, etc.) await storePendingFeedback({ message, context, timestamp: Date.now() }); } } // Flush queue when back online async function flushPendingFeedback() { const pending = await getPendingFeedback(); for (const item of pending) { await feedbackService.submitFeedback(item.message, item.context); await removePendingFeedback(item.id); } }

4. Collect Feedback at the Right Moments

Good times to prompt for feedback:

  • ✅ After completing a key action (purchase, signup, etc.)
  • ✅ After using a new feature
  • ✅ When the user explicitly taps “Send Feedback”
  • ✅ After recovering from an error

Avoid prompting:

  • ❌ During onboarding
  • ❌ Immediately after app launch
  • ❌ During time-sensitive actions

5. Respect User Privacy

// Only include user identifiers if you have consent const context = { ...deviceContext, // Only include if user opted in ...(hasAnalyticsConsent && { userId: currentUser.id }), ...(hasAnalyticsConsent && { userEmail: currentUser.email }), };

Rate Limits

PlanAPI Calls/MonthFeedback/Month
FREE300300
STARTER10,0005,000
PROUnlimitedUnlimited

If you exceed limits, the API returns a 403 Forbidden error.

Error Handling

Status CodeMeaningAction
201SuccessFeedback created
400Bad RequestCheck request body format
401UnauthorizedVerify API key
403ForbiddenCheck plan limits or project status
404Not FoundVerify project ID
500Server ErrorRetry with backoff

Notifications

When feedback is submitted via the API, all your configured notifications work automatically:

  • ✅ Email notifications
  • ✅ Slack notifications (STARTER+)
  • ✅ Webhook integrations

What’s Next?

FAQ

Q: Can I use the widget in a mobile app webview?
A: Yes, but the API approach gives you more control and a native feel.

Q: Do I need different API keys for iOS and Android?
A: No, you can use the same project and API key for both platforms.

Q: How do I identify which platform feedback came from?
A: Include platform in the context object. It will be visible in your dashboard.

Q: Can I attach screenshots?
A: Not yet via API. Screenshot support for mobile is on the roadmap.

Q: Is there an SDK I can use instead?
A: Not currently. The API is simple enough that a lightweight service class (as shown above) is recommended.


Need help? Check Troubleshooting or contact support through your dashboard.

Last updated on