This document describes the API integration in Leyu Mobile app. The app communicates with a RESTful API for all backend operations.
API configuration is managed through environment variables:
API_BASE_URL=http://your-api-url.com/apiSee ENVIRONMENT_SETUP.md for configuration details.
Location: lib/core/api/api_client.dart
The API client is built on Dio with the following features:
- Automatic token management
- Request/response interceptors
- Error handling
- Timeout configuration
- Retry logic
The app uses JWT (JSON Web Tokens) for authentication:
- Access Token: Short-lived token for API requests
- Refresh Token: Long-lived token for obtaining new access tokens
1. User logs in
↓
2. Receive access + refresh tokens
↓
3. Store tokens securely
↓
4. Include access token in API requests
↓
5. On 401 error, refresh access token
↓
6. Retry original request
// Login
POST /auth/login
Body: {
"phone": "+251912345678",
"password": "password123"
}
Response: {
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc...",
"user": { ... }
}
// Refresh Token
POST /auth/refresh-access-token
Body: {
"refreshToken": "eyJhbGc..."
}
Response: {
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc..."
}POST /auth/register
Body: {
"phone": "+251912345678"
}
Response: {
"verificationId": "uuid"
}
POST /auth/activate
Body: {
"verificationId": "uuid",
"phone": "+251912345678",
"otp": "123456"
}
Response: {
"accessToken": "...",
"user": { ... }
}
POST /auth/login
Body: {
"phone": "+251912345678",
"password": "password123"
}
Response: {
"accessToken": "...",
"refreshToken": "...",
"user": { ... }
}
POST /auth/request-otp
Body: {
"phone": "+251912345678"
}
Response: {
"success": true
}
POST /auth/reset-password
Body: {
"phone": "+251912345678",
"otp": "123456",
"newPassword": "newpassword123"
}
Response: {
"success": true
}
GET /tasks
Query Params:
- status: "available" | "in_progress" | "completed"
- type: "speech_to_text" | "text_to_speech" | "text_to_text"
- page: number
- limit: number
Response: {
"tasks": [
{
"id": "uuid",
"title": "Task Title",
"description": "Task Description",
"type": "speech_to_text",
"status": "available",
"reward": 10.50,
"deadline": "2026-02-01T00:00:00Z"
}
],
"total": 100,
"page": 1,
"limit": 20
}
GET /tasks/:id
Response: {
"id": "uuid",
"title": "Task Title",
"description": "Task Description",
"instructions": "Detailed instructions...",
"type": "speech_to_text",
"dataset": {
"id": "uuid",
"name": "Dataset Name"
},
"microTasks": [
{
"id": "uuid",
"content": "Sample text to read",
"status": "not_started"
}
]
}
POST /tasks/:id/submit
Body: FormData {
"microTaskId": "uuid",
"audioFile": File (for audio tasks),
"textContent": "transcribed text" (for text tasks)
}
Response: {
"success": true,
"submission": {
"id": "uuid",
"status": "under_review"
}
}
GET /profile
Response: {
"id": "uuid",
"firstName": "John",
"middleName": "Doe",
"lastName": "Smith",
"email": "john@example.com",
"phone": "+251912345678",
"profilePicture": "https://...",
"gender": "Male",
"birthDate": "1990-01-01",
"language": { ... },
"dialect": { ... }
}
PUT /profile
Body: {
"firstName": "John",
"middleName": "Doe",
"lastName": "Smith",
"email": "john@example.com"
}
Response: {
"success": true,
"user": { ... }
}
POST /profile/picture
Body: FormData {
"file": File
}
Response: {
"profilePicture": "https://..."
}
POST /profile/change-password
Body: {
"currentPassword": "oldpassword",
"newPassword": "newpassword"
}
Response: {
"success": true
}
GET /notifications/me
Query Params:
- page: number
- limit: number
Response: {
"notifications": [
{
"id": "uuid",
"title": "Notification Title",
"message": "Notification message",
"type": "task_assigned",
"isRead": false,
"createdAt": "2026-01-27T10:00:00Z"
}
],
"total": 50,
"unreadCount": 10
}
PUT /notifications/:id/read
Response: {
"success": true
}
PUT /notifications/read-all
Response: {
"success": true
}
GET /notifications/count-new
Response: {
"count": 10
}
GET /languages
Response: {
"languages": [
{
"id": "uuid",
"name": "Amharic",
"code": "am"
}
]
}
GET /dialects
Query Params:
- languageId: uuid
Response: {
"dialects": [
{
"id": "uuid",
"name": "Addis Ababa",
"languageId": "uuid"
}
]
}
All authenticated requests must include:
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json
{
"success": true,
"data": { ... },
"message": "Operation successful"
}{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Error description",
"details": { ... }
}
}200- Success201- Created400- Bad Request401- Unauthorized403- Forbidden404- Not Found422- Validation Error500- Internal Server Error
// Network errors
class NetworkFailure extends Failure {
NetworkFailure(String message);
}
// Server errors
class ServerFailure extends Failure {
ServerFailure(String message);
}
// Validation errors
class ValidationFailure extends Failure {
ValidationFailure(String message);
}
// Authentication errors
class AuthFailure extends Failure {
AuthFailure(String message);
}try {
final response = await apiClient.get('/endpoint');
return Right(response.data);
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
return Left(AuthFailure('Unauthorized'));
} else if (e.response?.statusCode == 404) {
return Left(NotFoundFailure('Resource not found'));
} else {
return Left(NetworkFailure(e.message ?? 'Network error'));
}
} catch (e) {
return Left(UnknownFailure(e.toString()));
}// Add authentication token
onRequest: (options, handler) {
final token = await getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
}// Handle token refresh
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Refresh token
final newToken = await refreshAccessToken();
// Retry request
final options = error.requestOptions;
options.headers['Authorization'] = 'Bearer $newToken';
final response = await dio.fetch(options);
return handler.resolve(response);
}
return handler.next(error);
}// Upload audio file
final formData = FormData.fromMap({
'microTaskId': taskId,
'audioFile': await MultipartFile.fromFile(
filePath,
filename: 'recording.m4a',
contentType: MediaType('audio', 'm4a'),
),
});
final response = await apiClient.post(
'/tasks/$taskId/submit',
data: formData,
);GET /tasks?page=1&limit=20{
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
}
}The API may implement rate limiting:
- Limit: 100 requests per minute per user
- Headers:
X-RateLimit-Limit: Total allowed requestsX-RateLimit-Remaining: Remaining requestsX-RateLimit-Reset: Time when limit resets
Cache-Control: max-age=3600, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// Cache GET requests
final cachedResponse = await cacheManager.get(url);
if (cachedResponse != null && !isExpired(cachedResponse)) {
return cachedResponse;
}
// Fetch from API
final response = await apiClient.get(url);
// Cache response
await cacheManager.put(url, response);// Mock successful response
when(mockApiClient.get('/tasks'))
.thenAnswer((_) async => Response(
data: {'tasks': []},
statusCode: 200,
));
// Mock error response
when(mockApiClient.get('/tasks'))
.thenThrow(DioException(
requestOptions: RequestOptions(path: '/tasks'),
response: Response(
statusCode: 401,
requestOptions: RequestOptions(path: '/tasks'),
),
));- HTTPS Only: All API calls over HTTPS
- Token Storage: Store tokens in secure storage
- Token Expiry: Implement automatic token refresh
- Input Validation: Validate all inputs before sending
- Error Messages: Don't expose sensitive information
- Rate Limiting: Respect API rate limits
- Timeout: Set appropriate timeouts
// Store tokens securely
await secureStorage.write(
key: 'access_token',
value: accessToken,
);
// Never log tokens
// ❌ Bad
print('Token: $accessToken');
// ✅ Good
logger.d('Token received');- Check if token is valid
- Verify token refresh logic
- Ensure Authorization header is set
- Verify endpoint URL
- Check API base URL configuration
- Ensure resource exists
- Check internet connection
- Increase timeout duration
- Implement retry logic
- Check request body format
- Verify required fields
- Validate data types
For API-related issues:
- Check this documentation
- Review error logs
- Test with API client (Postman, Insomnia)
- Contact backend team
Last Updated: January 27, 2026