# Mobile App Migration Guide - API v1 **Date:** January 14, 2026 **API Version:** 1.0.0 **Breaking Change:** Yes - All endpoints now require `/v1/` prefix --- ## Overview The API has been updated with versioning. All endpoints now use the `/api/v1/` prefix. This is a **breaking change** - the old routes without the version prefix have been removed. --- ## Quick Start ### Base URL Change ``` OLD: https://your-domain.com/api/ NEW: https://your-domain.com/api/v1/ ``` ### Update Your API Client **Before:** ```kotlin // Android (Kotlin) const val BASE_URL = "https://your-domain.com/api/" ``` **After:** ```kotlin // Android (Kotlin) const val BASE_URL = "https://your-domain.com/api/v1/" ``` **Before:** ```swift // iOS (Swift) let baseURL = "https://your-domain.com/api/" ``` **After:** ```swift // iOS (Swift) let baseURL = "https://your-domain.com/api/v1/" ``` **Before:** ```dart // Flutter (Dart) static const String baseUrl = 'https://your-domain.com/api/'; ``` **After:** ```dart // Flutter (Dart) static const String baseUrl = 'https://your-domain.com/api/v1/'; ``` --- ## Endpoint Migration Table ### Authentication | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | Login | `POST /api/login` | `POST /api/v1/login` | | Check Session | `POST /api/checksession` | `POST /api/v1/checksession` | | Delete Device | `POST /api/deletedevice` | `POST /api/v1/deletedevice` | ### Attendance (Absensi) | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | Check-in | `POST /api/absenmasuk` | `POST /api/v1/absenmasuk` | | Check-out | `POST /api/absenpulang` | `POST /api/v1/absenpulang` | | Get Today's Attendance | `POST /api/getabsen` | `POST /api/v1/getabsen` | | Get Office Location | `POST /api/kantor` | `POST /api/v1/kantor` | | Save Attendance | `POST /api/simpanabsen` | `POST /api/v1/simpanabsen` | ### Device Reset | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | Request Reset | `POST /api/device-reset/request` | `POST /api/v1/device-reset/request` | | My Requests | `POST /api/device-reset/my-request` | `POST /api/v1/device-reset/my-request` | ### Branches (Kantor) | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | List Branches | `GET /api/branches` | `GET /api/v1/branches` | | Get Branch by ID | `GET /api/branches/{id}` | `GET /api/v1/branches/{id}` | | Get Branch by Code | `POST /api/branches/by-kode` | `POST /api/v1/branches/by-kode` | ### Attendance Time Settings (Jam Absensi) | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | List All | `GET /api/jam-absensi` | `GET /api/v1/jam-absensi` | | Get Active | `GET /api/jam-absensi/active` | `GET /api/v1/jam-absensi/active` | | Get by ID | `GET /api/jam-absensi/{id}` | `GET /api/v1/jam-absensi/{id}` | ### Notifications | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | List Notifications | `GET /api/notifications` | `GET /api/v1/notifications` | | Unread Count | `GET /api/notifications/unread-count` | `GET /api/v1/notifications/unread-count` | | Mark as Read | `POST /api/notifications/{id}/read` | `POST /api/v1/notifications/{id}/read` | | Mark All as Read | `POST /api/notifications/read-all` | `POST /api/v1/notifications/read-all` | | Delete | `DELETE /api/notifications/{id}` | `DELETE /api/v1/notifications/{id}` | ### FCM Token Management | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | Register Token | `POST /api/fcm/register` | `POST /api/v1/fcm/register` | | Unregister Token | `POST /api/fcm/unregister` | `POST /api/v1/fcm/unregister` | ### Blog | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | Published Posts | `GET /api/blogs/published` | `GET /api/v1/blogs/published` | | Featured Posts | `GET /api/blogs/featured` | `GET /api/v1/blogs/featured` | | Get by Slug | `GET /api/blogs/slug/{slug}` | `GET /api/v1/blogs/slug/{slug}` | | Get Blog | `GET /api/getblog/{id}` | `GET /api/v1/getblog/{id}` | | Show Blog | `GET /api/showblog/{id}` | `GET /api/v1/showblog/{id}` | ### WebView | Action | Old Endpoint | New Endpoint | |--------|-------------|--------------| | Home View | `GET /api/webviewhome/{id}` | `GET /api/v1/webviewhome/{id}` | | Attendance List | `GET /api/webviewdataabsensi/{id}/{month}/{year}` | `GET /api/v1/webviewdataabsensi/{id}/{month}/{year}` | --- ## Response Format Changes ### Standardized Error Responses Error messages are now sanitized and don't expose internal details: **Before (Old):** ```json { "rcode": "99", "message": "Error: SQLSTATE[42S02]: Base table or view not found..." } ``` **After (New):** ```json { "rcode": "99", "message": "An error occurred. Please try again later." } ``` ### Response Codes Reference | Code | Meaning | |------|---------| | `00` | Success | | `01` | Validation/timing error | | `02` | Duplicate action (already checked in, etc.) | | `81` | Not found / Invalid | | `82` | Conflict (duplicate resource) | | `99` | Server error | ### Email Notification Response Change Device reset approve/reject responses now indicate queue status: **Before:** ```json { "rcode": "00", "message": "Device reset berhasil disetujui...", "data": {...}, "email_sent": true } ``` **After:** ```json { "rcode": "00", "message": "Device reset berhasil disetujui...", "data": { "request": {...}, "email_queued": true } } ``` --- ## New Features ### API Info Endpoint Get API information without authentication: ``` GET /api/ ``` Response: ```json { "name": "API Absensi Mobile", "version": "1.0.0", "current_api": "v1", "base_url": "https://your-domain.com/api/v1", "documentation": "https://your-domain.com/api/v1/docs" } ``` ### Improved Caching The following endpoints now have server-side caching for better performance: - `GET /api/v1/jam-absensi` - Cached for 1 hour - `GET /api/v1/jam-absensi/active` - Cached for 1 hour - `POST /api/v1/branches/by-kode` - Cached per branch for 1 hour - `GET /api/v1/branches?export=true` - Cached for 1 hour --- ## Testing Checklist Before releasing the updated mobile app, verify these flows: - [ ] Login flow works with new endpoint - [ ] Check-in (absen masuk) works correctly - [ ] Check-out (absen pulang) works correctly - [ ] Get today's attendance displays correctly - [ ] Office location/geofencing works - [ ] Device reset request can be submitted - [ ] Push notifications are received - [ ] FCM token registration on app start - [ ] FCM token unregistration on logout - [ ] Blog posts load correctly - [ ] Notification list displays - [ ] Mark notification as read works --- ## Rollout Strategy ### Recommended Approach 1. **Update mobile app** to use new `/api/v1/` endpoints 2. **Test thoroughly** in staging environment 3. **Release mobile app update** to app stores 4. **Force update** - Since this is a breaking change, consider implementing a force update mechanism ### Version Compatibility | Mobile App Version | API Version | Status | |-------------------|-------------|--------| | < 2.0.0 | No prefix | **BROKEN** - Will not work | | >= 2.0.0 | v1 | Supported | --- ## Support If you encounter any issues during migration: 1. Check that the base URL includes `/v1/` 2. Verify JWT token is being sent in Authorization header 3. Check the API info endpoint `/api/` is accessible 4. Review server logs for detailed error information --- ## Changelog ### v1.0.0 (January 14, 2026) - Added `/v1/` prefix to all API routes - Standardized response format with `ApiResponse` trait - Improved error handling (sanitized error messages) - Added caching for frequently accessed endpoints - Email notifications now sent via queue (non-blocking) - Push notification broadcasts now processed via queue - Added rate limiting on login (5 attempts per minute) - **NEW: Force Update feature** - Ensure users always have latest app version --- ## Force Update Implementation ### Overview The API now supports force update checking. Mobile apps should call the version check endpoint on startup **BEFORE** login to determine if an update is required. ### API Endpoint ``` POST /api/v1/app-version/check ``` **Request:** ```json { "platform": "android", "build_number": 10, "version_code": "1.0.0" } ``` **Response (Update Required):** ```json { "rcode": "00", "message": "Update required", "data": { "needs_update": true, "force_update": true, "current_build": 10, "latest_build": 20, "latest_version": "2.0.0", "min_build": 20, "min_version": "2.0.0", "maintenance_mode": false, "update_message": "Versi baru tersedia! Silakan update untuk melanjutkan.", "store_url": "https://play.google.com/store/apps/details?id=com.company.absensi", "changelog": "- Fitur baru\n- Perbaikan bug" } } ``` **Response (No Update):** ```json { "rcode": "00", "message": "Version check completed", "data": { "needs_update": false, "force_update": false, "current_build": 20, "latest_build": 20, "latest_version": "2.0.0", "maintenance_mode": false } } ``` **Response (Maintenance Mode):** ```json { "rcode": "00", "message": "App under maintenance", "data": { "needs_update": false, "force_update": false, "maintenance_mode": true, "maintenance_message": "Aplikasi sedang dalam maintenance. Silakan coba lagi dalam 1 jam." } } ``` ### Mobile App Implementation #### Flutter/Dart Example ```dart class VersionChecker { static const String VERSION_CHECK_URL = 'https://api.example.com/api/v1/app-version/check'; static Future checkVersion() async { final packageInfo = await PackageInfo.fromPlatform(); final response = await http.post( Uri.parse(VERSION_CHECK_URL), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'platform': Platform.isAndroid ? 'android' : 'ios', 'build_number': int.parse(packageInfo.buildNumber), 'version_code': packageInfo.version, }), ); final data = jsonDecode(response.body); return VersionCheckResult.fromJson(data['data']); } static void handleVersionCheck(BuildContext context) async { final result = await checkVersion(); if (result.maintenanceMode) { // Show maintenance dialog (no dismiss) showMaintenanceDialog(context, result.maintenanceMessage); return; } if (result.forceUpdate) { // Show force update dialog (no dismiss, only update button) showForceUpdateDialog( context, message: result.updateMessage, storeUrl: result.storeUrl, ); return; } if (result.needsUpdate) { // Show optional update dialog (can dismiss) showOptionalUpdateDialog( context, message: result.updateMessage, storeUrl: result.storeUrl, changelog: result.changelog, ); } // Continue to login/main screen } } ``` #### Android/Kotlin Example ```kotlin class VersionChecker(private val context: Context) { suspend fun checkVersion(): VersionCheckResult { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val buildNumber = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageInfo.longVersionCode.toInt() } else { @Suppress("DEPRECATION") packageInfo.versionCode } val request = VersionCheckRequest( platform = "android", buildNumber = buildNumber, versionCode = packageInfo.versionName ) return apiService.checkVersion(request) } fun handleResult(result: VersionCheckResult, activity: Activity) { when { result.maintenanceMode -> { showMaintenanceDialog(activity, result.maintenanceMessage) } result.forceUpdate -> { showForceUpdateDialog(activity, result.updateMessage, result.storeUrl) } result.needsUpdate -> { showOptionalUpdateDialog(activity, result.updateMessage, result.storeUrl) } else -> { // Proceed to login } } } } ``` #### iOS/Swift Example ```swift class VersionChecker { func checkVersion() async throws -> VersionCheckResult { let buildNumber = Int(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0") ?? 0 let versionCode = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" let request = VersionCheckRequest( platform: "ios", buildNumber: buildNumber, versionCode: versionCode ) return try await APIService.shared.checkVersion(request) } func handleResult(_ result: VersionCheckResult, presenter: UIViewController) { if result.maintenanceMode { showMaintenanceAlert(presenter, message: result.maintenanceMessage) return } if result.forceUpdate { showForceUpdateAlert(presenter, message: result.updateMessage, storeURL: result.storeUrl) return } if result.needsUpdate { showOptionalUpdateAlert(presenter, message: result.updateMessage, storeURL: result.storeUrl) } } } ``` ### App Startup Flow ``` ┌─────────────────┐ │ App Launch │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Check Version │ ◄── POST /api/v1/app-version/check └────────┬────────┘ │ ▼ ┌────────────┐ │Maintenance?│ └─────┬──────┘ │ Yes ▼ ┌─────────────────┐ │Show Maintenance │ │ Dialog │ (No dismiss, app blocked) └─────────────────┘ │ No ▼ ┌────────────┐ │Force Update│ └─────┬──────┘ │ Yes ▼ ┌─────────────────┐ │Show Force Update│ │ Dialog │ (Only "Update" button → Store) └─────────────────┘ │ No ▼ ┌────────────┐ │Needs Update│ └─────┬──────┘ │ Yes ▼ ┌─────────────────┐ │ Show Optional │ │ Update Dialog │ (Can dismiss with "Later") └────────┬────────┘ │ No / Later ▼ ┌─────────────────┐ │ Login Page │ └─────────────────┘ ``` ### Admin Dashboard Admins can manage app versions via the API: | Endpoint | Method | Description | |----------|--------|-------------| | `/api/v1/app-versions` | GET | List all version configs | | `/api/v1/app-versions` | POST | Create/update version config | | `/api/v1/app-versions/{id}` | GET | Get specific config | | `/api/v1/app-versions/{id}` | PUT | Update config | | `/api/v1/app-versions/{id}/toggle-force-update` | POST | Quick toggle force update | | `/api/v1/app-versions/{id}/toggle-maintenance` | POST | Quick toggle maintenance mode | ### Database Setup Run the migration: ```bash php artisan migrate ``` Seed initial data: ```bash php artisan db:seed --class=AppVersionSeeder ```