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)
- Create a project in your Miniback dashboard
- Generate an API key: Project Settings → API Access → Generate Key
- POST feedback to:
https://miniback.io/api/projects/{projectId}/feedback- Include
x-api-keyheader with your API keyAll 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
- Sign in to your Miniback dashboard
- Navigate to your project
- Scroll to the API Access section
- Click “Generate API Key” if you don’t have one
- 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}/feedbackRequest Headers
| Header | Value | Required |
|---|---|---|
x-api-key | Your API key (mbk_...) | ✅ Yes |
Content-Type | application/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"
}
}| Field | Type | Required | Description |
|---|---|---|---|
message | string | ✅ Yes | The feedback message from the user |
context | object | ❌ No | Additional 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");Recommended Context Fields
⚠️ 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:
| Field | Type | Description | Example |
|---|---|---|---|
url | string | Deep link or screen identifier | "myapp://checkout", "CheckoutScreen" |
userAgent | string | App and device identifier string | "MyApp/2.1.0 (iOS 17.2; iPhone 15 Pro)" |
timestamp | string | ISO 8601 timestamp | "2025-01-15T10:30:00.000Z" |
screen | object | Device screen dimensions | { "width": 1179, "height": 2556 } |
Mobile-Specific Fields
Additional fields useful for mobile debugging and analytics:
| Field | Type | Description | Example |
|---|---|---|---|
platform | string | Operating system | "iOS", "Android" |
appVersion | string | Your app version | "2.1.0" |
osVersion | string | OS version | "17.2", "14" |
deviceModel | string | Device model | "iPhone 15 Pro", "Pixel 8" |
screenName | string | Current screen/view name | "CheckoutScreen" |
userId | string | Your internal user ID | "user_abc123" |
userEmail | string | User’s email (if consented) | "user@example.com" |
sessionId | string | Session identifier | "sess_xyz789" |
locale | string | User’s language/region | "en-US" |
buildNumber | string | App build number | "142" |
Tip: The
userAgentfield 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
| Plan | API Calls/Month | Feedback/Month |
|---|---|---|
| FREE | 300 | 300 |
| STARTER | 10,000 | 5,000 |
| PRO | Unlimited | Unlimited |
If you exceed limits, the API returns a 403 Forbidden error.
Error Handling
| Status Code | Meaning | Action |
|---|---|---|
201 | Success | Feedback created |
400 | Bad Request | Check request body format |
401 | Unauthorized | Verify API key |
403 | Forbidden | Check plan limits or project status |
404 | Not Found | Verify project ID |
500 | Server Error | Retry 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?
- API Reference — Full API documentation
- API Security — Best practices for API keys
- Feedback Workflow — Manage incoming feedback
- Analytics — Track feedback trends (STARTER+)
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.