Who is this for?
Mobile developers using React Native or Flutter who want to accept payments,
create payment links, or manage wallets with Monieswitch APIs.
Overview
This guide covers:- Setting up secure API integration architecture
- Creating a backend proxy for Monieswitch APIs
- Building mobile app clients that communicate with your backend
- Creating payment links and handling responses
- Security best practices for mobile payments
- Error handling and user experience considerations
Prerequisites
1
Create Monieswitch Account
A Monieswitch account. Register at Monieswitch
Dashboard.
2
Get API Key
API Key from your Monieswitch dashboard.
3
Set Up Development Environment
React Native or Flutter development environment set up.
4
Backend Service
A backend service (Node.js, Python, etc.) to securely handle Monieswitch API calls.
Architecture Overview
Copy
Mobile App β Your Backend API β Monieswitch APIs
Mobile apps should never communicate directly with Monieswitch APIs. Always use a secure backend to proxy requests and protect your API keys.
1. Backend Setup (Required)
First, set up your backend to handle Monieswitch API calls:Environment Configuration
Add these to your backend.env file:
Copy
MONIESWITCH_BASE_URL=https://nini.monieswitch.com
MONIESWITCH_API_KEY=your_api_key_here
PORT=3000
Backend Implementation
- Node.js/Express
server.js
Copy
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const BASE_URL = process.env.MONIESWITCH_BASE_URL;
const API_KEY = process.env.MONIESWITCH_API_KEY;
// Get merchant details
app.get('/api/monieswitch/merchant', async (req, res) => {
try {
const response = await axios.get(`${BASE_URL}/merchant`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
res.json(response.data);
} catch (error) {
console.error('Monieswitch API Error:', error.response?.data);
res.status(error.response?.status || 500).json({
error: 'Failed to fetch merchant details'
});
}
});
// Create payment link
app.post('/api/monieswitch/payment-links', async (req, res) => {
try {
const { amount, currency, description, email } = req.body;
// Validate required fields
if (!amount || !currency || !email) {
return res.status(400).json({
error: 'Missing required fields: amount, currency, email'
});
}
const payload = {
amount: parseInt(amount),
currency,
description: description || 'Mobile payment',
email
};
const response = await axios.post(`${BASE_URL}/payment-links`, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
});
res.json(response.data);
} catch (error) {
console.error('Payment Link Error:', error.response?.data);
res.status(error.response?.status || 500).json({
error: 'Failed to create payment link'
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Backend server running on port ${PORT}`);
});
2. Mobile App Setup
Install HTTP Request Libraries
- React Native
- Flutter
Copy
npm install axios
# For secure storage (optional)
npm install @react-native-async-storage/async-storage
pubspec.yaml
Copy
dependencies:
http: ^1.1.0
# For secure storage (optional)
flutter_secure_storage: ^9.0.0
Copy
flutter pub get
Configure API Client
- React Native
- Flutter
Create an API service file:
services/api.js
Copy
import axios from 'axios';
// Configure your backend URL
const BACKEND_BASE_URL = 'https://your-backend.com'; // Replace with your backend URL
// For local development: 'http://localhost:3000'
const apiClient = axios.create({
baseURL: BACKEND_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Add request interceptor for logging
apiClient.interceptors.request.use(
(config) => {
console.log('API Request:', config.method?.toUpperCase(), config.url);
return config;
},
(error) => Promise.reject(error)
);
// Add response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
export const monieswitchAPI = {
// Get merchant details
getMerchantDetails: async () => {
try {
const response = await apiClient.get('/api/monieswitch/merchant');
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || 'Failed to fetch merchant details');
}
},
// Create payment link
createPaymentLink: async (paymentData) => {
try {
const response = await apiClient.post('/api/monieswitch/payment-links', paymentData);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || 'Failed to create payment link');
}
},
};
Create an API service class:
lib/services/monieswitch_service.dart
Copy
import 'package:http/http.dart' as http;
import 'dart:convert';
class MonieswitchService {
// Configure your backend URL
static const String _baseUrl = 'https://your-backend.com'; // Replace with your backend URL
// For local development: 'http://localhost:3000'
static const Duration _timeout = Duration(seconds: 10);
static Future<Map<String, dynamic>> _makeRequest(
String method,
String endpoint,
{Map<String, dynamic>? body}
) async {
final uri = Uri.parse('$_baseUrl$endpoint');
final headers = {'Content-Type': 'application/json'};
print('API Request: $method $endpoint');
http.Response response;
try {
switch (method.toUpperCase()) {
case 'GET':
response = await http.get(uri, headers: headers).timeout(_timeout);
break;
case 'POST':
response = await http.post(
uri,
headers: headers,
body: body != null ? json.encode(body) : null,
).timeout(_timeout);
break;
default:
throw Exception('Unsupported HTTP method: $method');
}
} catch (e) {
throw Exception('Network error: ${e.toString()}');
}
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return responseData;
} else {
final errorMessage = responseData['error'] ?? 'Unknown error occurred';
throw Exception(errorMessage);
}
}
// Get merchant details
static Future<Map<String, dynamic>> getMerchantDetails() async {
return await _makeRequest('GET', '/api/monieswitch/merchant');
}
// Create payment link
static Future<Map<String, dynamic>> createPaymentLink({
required int amount,
required String currency,
required String email,
String? description,
}) async {
final body = {
'amount': amount,
'currency': currency,
'email': email,
if (description != null) 'description': description,
};
return await _makeRequest('POST', '/api/monieswitch/payment-links', body: body);
}
}
3. Using the API in Your App
Fetching Merchant Details
- React Native
- Flutter
components/MerchantInfo.js
Copy
import React, { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator, Alert } from 'react-native';
import { monieswitchAPI } from '../services/api';
const MerchantInfo = () => {
const [merchantData, setMerchantData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchMerchantDetails();
}, []);
const fetchMerchantDetails = async () => {
try {
setLoading(true);
const data = await monieswitchAPI.getMerchantDetails();
setMerchantData(data);
} catch (error) {
Alert.alert('Error', error.message);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<View>
<Text>Merchant: {merchantData?.name}</Text>
<Text>Email: {merchantData?.email}</Text>
</View>
);
};
export default MerchantInfo;
lib/widgets/merchant_info.dart
Copy
import 'package:flutter/material.dart';
import '../services/monieswitch_service.dart';
class MerchantInfo extends StatefulWidget {
@override
_MerchantInfoState createState() => _MerchantInfoState();
}
class _MerchantInfoState extends State<MerchantInfo> {
Map<String, dynamic>? merchantData;
bool isLoading = true;
String? errorMessage;
@override
void initState() {
super.initState();
fetchMerchantDetails();
}
Future<void> fetchMerchantDetails() async {
try {
setState(() {
isLoading = true;
errorMessage = null;
});
final data = await MonieswitchService.getMerchantDetails();
setState(() {
merchantData = data;
isLoading = false;
});
} catch (e) {
setState(() {
errorMessage = e.toString();
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
if (errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $errorMessage', style: TextStyle(color: Colors.red)),
ElevatedButton(
onPressed: fetchMerchantDetails,
child: Text('Retry'),
),
],
),
);
}
return Column(
children: [
Text('Merchant: ${merchantData?['name'] ?? 'N/A'}'),
Text('Email: ${merchantData?['email'] ?? 'N/A'}'),
],
);
}
}
Creating Payment Links
- React Native
- Flutter
components/PaymentLinkForm.js
Copy
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, Alert, ActivityIndicator } from 'react-native';
import { monieswitchAPI } from '../services/api';
const PaymentLinkForm = () => {
const [formData, setFormData] = useState({
amount: '',
currency: 'NGN',
description: '',
email: '',
});
const [loading, setLoading] = useState(false);
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const createPaymentLink = async () => {
if (!formData.amount || !formData.email) {
Alert.alert('Error', 'Please fill in amount and email');
return;
}
try {
setLoading(true);
const paymentData = {
amount: parseInt(formData.amount),
currency: formData.currency,
description: formData.description || 'Mobile payment',
email: formData.email,
};
const response = await monieswitchAPI.createPaymentLink(paymentData);
Alert.alert(
'Success',
`Payment link created! URL: ${response.payment_url}`,
[{ text: 'OK' }]
);
// Reset form
setFormData({ amount: '', currency: 'NGN', description: '', email: '' });
} catch (error) {
Alert.alert('Error', error.message);
} finally {
setLoading(false);
}
};
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, marginBottom: 20 }}>Create Payment Link</Text>
<TextInput
placeholder="Amount (e.g., 5000)"
value={formData.amount}
onChangeText={(value) => handleInputChange('amount', value)}
keyboardType="numeric"
style={{ borderWidth: 1, padding: 10, marginBottom: 10, borderRadius: 5 }}
/>
<TextInput
placeholder="Currency (e.g., NGN)"
value={formData.currency}
onChangeText={(value) => handleInputChange('currency', value)}
style={{ borderWidth: 1, padding: 10, marginBottom: 10, borderRadius: 5 }}
/>
<TextInput
placeholder="Description (optional)"
value={formData.description}
onChangeText={(value) => handleInputChange('description', value)}
style={{ borderWidth: 1, padding: 10, marginBottom: 10, borderRadius: 5 }}
/>
<TextInput
placeholder="Customer Email"
value={formData.email}
onChangeText={(value) => handleInputChange('email', value)}
keyboardType="email-address"
style={{ borderWidth: 1, padding: 10, marginBottom: 20, borderRadius: 5 }}
/>
<TouchableOpacity
onPress={createPaymentLink}
disabled={loading}
style={{
backgroundColor: loading ? '#ccc' : '#007AFF',
padding: 15,
borderRadius: 5,
alignItems: 'center'
}}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={{ color: 'white', fontSize: 16 }}>Create Payment Link</Text>
)}
</TouchableOpacity>
</View>
);
};
export default PaymentLinkForm;
lib/widgets/payment_link_form.dart
Copy
import 'package:flutter/material.dart';
import '../services/monieswitch_service.dart';
class PaymentLinkForm extends StatefulWidget {
@override
_PaymentLinkFormState createState() => _PaymentLinkFormState();
}
class _PaymentLinkFormState extends State<PaymentLinkForm> {
final _formKey = GlobalKey<FormState>();
final _amountController = TextEditingController();
final _currencyController = TextEditingController(text: 'NGN');
final _descriptionController = TextEditingController();
final _emailController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_amountController.dispose();
_currencyController.dispose();
_descriptionController.dispose();
_emailController.dispose();
super.dispose();
}
Future<void> _createPaymentLink() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
final response = await MonieswitchService.createPaymentLink(
amount: int.parse(_amountController.text),
currency: _currencyController.text,
email: _emailController.text,
description: _descriptionController.text.isEmpty
? null
: _descriptionController.text,
);
if (mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Success'),
content: Text('Payment link created!\n\nURL: ${response['payment_url']}'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
// Reset form
_amountController.clear();
_currencyController.text = 'NGN';
_descriptionController.clear();
_emailController.clear();
}
} catch (e) {
if (mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(e.toString().replaceFirst('Exception: ', '')),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Create Payment Link',
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 20),
TextFormField(
controller: _amountController,
decoration: InputDecoration(
labelText: 'Amount',
hintText: 'e.g., 5000',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an amount';
}
if (int.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _currencyController,
decoration: InputDecoration(
labelText: 'Currency',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a currency';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: 'Description (optional)',
border: OutlineInputBorder(),
),
),
SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Customer Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _createPaymentLink,
child: _isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text('Create Payment Link'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
);
}
}
4. Error Handling Best Practices
Common Error Scenarios
- Network connectivity issues
- Invalid API responses
- Authentication failures
- Validation errors
- Server timeouts
Implementation Examples
- React Native
- Flutter
utils/errorHandler.js
Copy
export const handleAPIError = (error, showAlert = true) => {
let errorMessage = 'An unexpected error occurred';
if (error.response) {
// Server responded with error status
errorMessage = error.response.data?.error || `Server error: ${error.response.status}`;
} else if (error.request) {
// Request was made but no response received
errorMessage = 'Network error. Please check your connection.';
} else {
// Something else happened
errorMessage = error.message || errorMessage;
}
console.error('API Error:', errorMessage);
if (showAlert) {
Alert.alert('Error', errorMessage);
}
return errorMessage;
};
lib/utils/error_handler.dart
Copy
import 'package:flutter/material.dart';
class ErrorHandler {
static void handleError(BuildContext context, dynamic error) {
String errorMessage = 'An unexpected error occurred';
if (error is Exception) {
String exceptionString = error.toString();
if (exceptionString.startsWith('Exception: ')) {
errorMessage = exceptionString.substring(11);
} else {
errorMessage = exceptionString;
}
}
print('Error: $errorMessage');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
}
5. Security Best Practices
- Never store API keys in mobile app code
- Always use HTTPS for all communications
- Implement proper input validation
- Use certificate pinning for production apps
- Implement request signing for sensitive operations
Additional Security Measures
- Input Validation: Always validate user input before sending to your backend
- Rate Limiting: Implement rate limiting on your backend API endpoints
- Authentication: Use proper authentication mechanisms between your app and backend
- Logging: Log API requests and responses for debugging (exclude sensitive data)
- Error Messages: Donβt expose internal system details in error messages
6. Testing Your Integration
Backend Testing
Copy
# Test merchant details endpoint
curl -X GET http://localhost:3000/api/monieswitch/merchant
# Test payment link creation
curl -X POST http://localhost:3000/api/monieswitch/payment-links \
-H "Content-Type: application/json" \
-d '{"amount": 5000, "currency": "NGN", "email": "[email protected]"}'
Next Steps
- Explore more endpoints in the API Reference
- Implement webhooks on your backend for real-time payment updates
Troubleshooting
Common Issues
- CORS errors: Ensure your backend properly handles CORS if testing from web
- Network timeouts: Implement proper timeout handling and retry logic
- Environment variables not loading: Double-check your
.envfile location and format - API key authentication failures: Verify your API key is correct and has proper permissions
Debug Tips
- Enable detailed logging in both mobile app and backend
- Use network debugging tools (React Native Debugger, Flutter Inspector)
- Test API endpoints independently before integrating with mobile app
- Monitor backend logs for detailed error informati