Auth
Endpoint untuk autentikasi user. Fumai menggunakan unified auth flow — satu alur untuk login dan register.
POST /auth/check-email
Cek apakah email sudah terdaftar dan metode auth apa yang tersedia. Langkah pertama dalam unified auth flow.
Tidak memerlukan authentication.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
email | string | Ya | Email yang ingin dicek |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/check-email \
-H "Content-Type: application/json" \
-d '{ "email": "user@example.com" }'const res = await fetch('https://fumai.app/api/mobile/v1/auth/check-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' }),
})
const data = await res.json()Response — Email Belum Terdaftar
{
"success": true,
"exists": false,
"method": "register"
}Response — Email Terdaftar (Credentials)
{
"success": true,
"exists": true,
"method": "credentials",
"hasGoogle": true,
"hasApple": false,
"isVerified": true
}Response — Email Terdaftar (OAuth Only)
{
"success": true,
"exists": true,
"method": "google",
"hasGoogle": true,
"hasApple": false,
"isVerified": true
}Response — Email Belum Diverifikasi
{
"success": true,
"exists": true,
"method": "credentials",
"hasGoogle": false,
"hasApple": false,
"isVerified": false,
"userId": "clx..."
}Response Fields
| Field | Tipe | Keterangan |
|---|---|---|
exists | boolean | true jika email sudah terdaftar |
method | string | "register", "credentials", "google", atau "apple" |
hasGoogle | boolean | User memiliki Google OAuth |
hasApple | boolean | User memiliki Apple Sign In |
isVerified | boolean | Email sudah diverifikasi |
userId | string? | User ID (hanya jika isVerified: false) |
Behavior
| Skenario | method | Aksi |
|---|---|---|
| Email belum terdaftar | register | Auto-register → kirim OTP → verify |
| User punya password | credentials | Tampilkan form login |
| User hanya punya Google | google | Tampilkan Google Sign In |
| User hanya punya Apple | apple | Tampilkan Apple Sign In |
| Email belum verified | credentials | Resend OTP → verify |
Error Codes
| Code | Status | Keterangan |
|---|---|---|
INVALID_EMAIL | 400 | Format email tidak valid |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 10 per menit (per IP)
POST /auth/login
Login dengan email + password. Dipanggil setelah check-email mengembalikan method: "credentials".
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
email | string | Ya | Email user |
password | string | Ya | Password user |
deviceInfo | string | Tidak | Device identifier |
deviceName | string | Tidak | Nama device |
platform | string | Tidak | "android" atau "ios" |
appVersion | string | Tidak | Versi aplikasi |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123",
"platform": "android",
"appVersion": "1.0.0"
}'const res = await fetch('https://fumai.app/api/mobile/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
password: 'password123',
platform: 'android',
appVersion: '1.0.0',
}),
})
const data = await res.json()Response 200
{
"success": true,
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"expiresIn": 900,
"user": {
"id": "clx...",
"name": "John Doe",
"email": "user@example.com",
"image": "https://...",
"role": "USER",
"emailVerified": true,
"createdAt": "2026-03-04T...",
"profile": {
"gender": "MALE",
"birthDate": "1995-06-15T00:00:00.000Z",
"height": 175,
"heightUnit": "cm",
"weight": 70,
"weightUnit": "kg",
"fitnessGoal": "BUILD_MUSCLE",
"onboardingCompleted": true,
"experience": "INTERMEDIATE"
},
"nutritionGoal": {
"dailyCalories": 2200,
"proteinTarget": 150,
"carbsTarget": 250,
"fatTarget": 70
}
}
}Info:
nutritionGoalbisanulljika user belum setup nutrition.profilebisanulljika profil belum dibuat.
Error Codes
| Code | Status | Keterangan |
|---|---|---|
VALIDATION_ERROR | 400 | Input tidak valid |
INVALID_CREDENTIALS | 401 | Email atau password salah |
GOOGLE_ACCOUNT | 401 | User terdaftar via Google, tidak punya password |
EMAIL_NOT_VERIFIED | 403 | Email belum diverifikasi |
RATE_LIMITED | 429 | Terlalu banyak percobaan |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 5 per menit (per IP)
POST /auth/register
Register user baru. Dipanggil otomatis setelah check-email mengembalikan method: "register". Akun dibuat dengan email saja (tanpa form register terpisah), lalu diarahkan ke verifikasi OTP.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
email | string | Ya | Email user (maks 254 karakter, auto-lowercase) |
password | string | Tidak | Min 8 karakter, harus ada huruf kecil + besar + angka. Jika tidak dikirim, akun dibuat tanpa password |
name | string | Tidak | Min 2, maks 100 karakter. Default: email prefix |
referralCode | string | Tidak | Kode referral (auto-uppercase, kode invalid di-ignore) |
deviceInfo | string | Tidak | Device identifier |
platform | string | Tidak | "android" atau "ios" |
appVersion | string | Tidak | Versi aplikasi |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"platform": "android",
"appVersion": "1.0.0"
}'const res = await fetch('https://fumai.app/api/mobile/v1/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'user@example.com',
platform: 'android',
appVersion: '1.0.0',
}),
})
const data = await res.json()Response 201
{
"success": true,
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"expiresIn": 900,
"requiresEmailVerification": true,
"user": {
"id": "clx...",
"email": "user@example.com",
"name": "user",
"image": null,
"role": "USER",
"emailVerified": false,
"createdAt": "2026-03-04T...",
"profile": {
"gender": null,
"birthDate": null,
"height": null,
"weight": null,
"fitnessGoal": null,
"onboardingCompleted": false,
"experience": "BEGINNER"
}
}
}Behavior Setelah Register
| Aksi | Detail |
|---|---|
| Auto-login | JWT tokens langsung diberikan |
| Profile dibuat | Profile otomatis dibuat dengan onboardingCompleted: false |
| OTP dikirim | Kode OTP verifikasi dikirim otomatis ke email |
| Referral code | Jika valid → buat redemption. Jika invalid → di-ignore |
Error Codes
| Code | Status | Keterangan |
|---|---|---|
VALIDATION_ERROR | 400 | Input tidak valid |
EMAIL_EXISTS | 409 | Email sudah terdaftar |
RATE_LIMITED | 429 | Terlalu banyak percobaan |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 3 per jam (per IP)
POST /auth/verify-email
Verifikasi email menggunakan kode OTP 6 digit. Dipanggil setelah register atau saat email belum diverifikasi.
Tidak memerlukan authentication.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
userId | string | Ya | User ID dari register/check-email response |
code | string | Ya | Kode OTP 6 digit dari email |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{
"userId": "clx...",
"code": "123456"
}'const res = await fetch('https://fumai.app/api/mobile/v1/auth/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'clx...', code: '123456' }),
})
const data = await res.json()Response — Berhasil
{
"success": true,
"message": "Email berhasil diverifikasi",
"isNewUser": true
}Response — Sudah Terverifikasi
{
"success": true,
"alreadyVerified": true,
"message": "Email sudah terverifikasi"
}Error Codes
| Code | Status | Keterangan |
|---|---|---|
VALIDATION_ERROR | 400 | Input tidak valid |
USER_NOT_FOUND | 404 | User tidak ditemukan |
INVALID_CODE | 400 | Kode OTP salah atau kadaluarsa (24 jam) |
RATE_LIMITED | 429 | Terlalu banyak percobaan |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 10 per 15 menit (per IP)
POST /auth/resend-verification
Kirim ulang kode OTP verifikasi email.
Tidak memerlukan authentication.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
email | string | Ya | Email user |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/resend-verification \
-H "Content-Type: application/json" \
-d '{ "email": "user@example.com" }'const res = await fetch('https://fumai.app/api/mobile/v1/auth/resend-verification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' }),
})
const data = await res.json()Response — Berhasil
{
"success": true,
"message": "Kode verifikasi telah dikirim ke email kamu.",
"userId": "clx..."
}Response — Sudah Terverifikasi
{
"success": true,
"message": "Email sudah terverifikasi. Silakan login.",
"alreadyVerified": true
}Info: Selalu return
success: truemeskipun email tidak terdaftar (untuk keamanan). Kode OTP berlaku 24 jam.
Error Codes
| Code | Status | Keterangan |
|---|---|---|
VALIDATION_ERROR | 400 | Email tidak valid |
EMAIL_FAILED | 500 | Gagal mengirim email |
RATE_LIMITED | 429 | Terlalu banyak request |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 2 per 5 menit (per IP)
POST /auth/google
Login atau register menggunakan Google ID token.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
idToken | string | Ya | Google ID token |
deviceInfo | string | Tidak | Device identifier |
deviceName | string | Tidak | Nama device |
platform | string | Tidak | "android" atau "ios" |
appVersion | string | Tidak | Versi aplikasi |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/google \
-H "Content-Type: application/json" \
-d '{
"idToken": "eyJhbGciOiJSUzI1NiIs...",
"platform": "android",
"appVersion": "1.0.0"
}'const res = await fetch('https://fumai.app/api/mobile/v1/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
idToken: googleIdToken,
platform: 'android',
appVersion: '1.0.0',
}),
})
const data = await res.json()Response 200 / 201
{
"success": true,
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"expiresIn": 900,
"isNewUser": true,
"user": {
"id": "clx...",
"email": "user@gmail.com",
"name": "John Doe",
"image": "https://lh3.googleusercontent.com/...",
"role": "USER",
"emailVerified": true,
"profile": {
"onboardingCompleted": false,
"experience": "BEGINNER"
}
}
}Behavior
| Skenario | Aksi | Status |
|---|---|---|
| Email baru | Auto-create user + profile | 201 |
| Email terdaftar (verified) | Link Google account + login | 200 |
| Email terdaftar (unverified) | Tolak — harus verify dulu | 401 |
Error Codes
| Code | Status | Keterangan |
|---|---|---|
INVALID_GOOGLE_TOKEN | 401 | Token tidak valid atau expired |
GOOGLE_EMAIL_NOT_VERIFIED | 401 | Email Google belum diverifikasi |
ACCOUNT_NOT_VERIFIED | 401 | Email Fumai belum diverifikasi |
RATE_LIMITED | 429 | Terlalu banyak percobaan |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 10 per 15 menit (per IP)
POST /auth/apple
Login atau register menggunakan Apple identity token.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
identityToken | string | Ya | Apple identity token (JWT) |
fullName | object | Tidak | { givenName, familyName } — hanya dikirim Apple pada pertama kali |
email | string | Tidak | Email dari Apple (hanya pada first auth) |
authorizationCode | string | Tidak | Authorization code dari Apple |
deviceInfo | string | Tidak | Device identifier |
platform | string | Tidak | "android" atau "ios" |
appVersion | string | Tidak | Versi aplikasi |
Peringatan: Apple hanya mengirim
fullNamedanidentityTokenyang tersedia. Client harus menyimpanfullNamepada first auth.
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/apple \
-H "Content-Type: application/json" \
-d '{
"identityToken": "eyJraWQiOiJXNldjT0...",
"fullName": { "givenName": "John", "familyName": "Doe" },
"email": "user@privaterelay.appleid.com",
"platform": "ios",
"appVersion": "1.0.0"
}'const res = await fetch('https://fumai.app/api/mobile/v1/auth/apple', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identityToken: appleIdentityToken,
fullName: { givenName: 'John', familyName: 'Doe' },
email: credential.email,
platform: 'ios',
appVersion: '1.0.0',
}),
})
const data = await res.json()Response 200 / 201
{
"success": true,
"accessToken": "eyJhbGciOi...",
"refreshToken": "eyJhbGciOi...",
"expiresIn": 900,
"isNewUser": true,
"user": {
"id": "clx...",
"email": "user@privaterelay.appleid.com",
"name": "John Doe",
"image": null,
"role": "USER",
"emailVerified": true,
"profile": {
"onboardingCompleted": false,
"experience": "BEGINNER"
}
}
}Perbedaan dengan Google OAuth
| Aspek | Apple | |
|---|---|---|
| Profile image | Ya | Tidak |
| Name | Selalu tersedia | Hanya pada first auth |
| Selalu real email | Bisa relay email |
Error Codes
| Code | Status | Keterangan |
|---|---|---|
INVALID_APPLE_TOKEN | 401 | Token tidak valid atau expired |
ACCOUNT_NOT_VERIFIED | 401 | Email Fumai belum diverifikasi |
APPLE_EMAIL_REQUIRED | 400 | User harus memberikan izin email |
RATE_LIMITED | 429 | Terlalu banyak percobaan |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 10 per 15 menit (per IP)
POST /auth/refresh
Refresh access token menggunakan refresh token.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
refreshToken | string | Ya | Refresh token yang valid |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "eyJhbGciOi..." }'const res = await fetch('https://fumai.app/api/mobile/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
})
const data = await res.json()Response 200
{
"success": true,
"accessToken": "eyJhbGciOi...(new)",
"refreshToken": "eyJhbGciOi...(new)",
"expiresIn": 900
}Info: Refresh token bersifat one-time use. Token lama otomatis di-revoke setelah dipakai.
Error Codes
| Code | Status | Keterangan |
|---|---|---|
VALIDATION_ERROR | 400 | Refresh token tidak dikirim |
INVALID_REFRESH_TOKEN | 401 | Token tidak valid atau expired |
REFRESH_FAILED | 401 | Gagal memperbarui token — user harus login ulang |
RATE_LIMITED | 429 | Terlalu banyak permintaan |
SERVER_ERROR | 500 | Internal server error |
Rate Limit: 30 per menit (per IP)
GET /auth/me
Mendapatkan data user yang sedang login.
Memerlukan JWT authentication.
Contoh Request
curl -X GET https://fumai.app/api/mobile/v1/auth/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"const res = await fetch('https://fumai.app/api/mobile/v1/auth/me', {
headers: { Authorization: `Bearer ${accessToken}` },
})
const data = await res.json()Response 200
{
"success": true,
"user": {
"id": "clx...",
"name": "John Doe",
"email": "user@example.com",
"image": "https://...",
"role": "USER",
"emailVerified": true,
"referralCode": "JOHNDOE",
"createdAt": "2026-03-04T...",
"updatedAt": "2026-03-10T...",
"profile": {
"gender": "MALE",
"birthDate": "1995-06-15T00:00:00.000Z",
"height": 175,
"heightUnit": "cm",
"weight": 70,
"weightUnit": "kg",
"fitnessGoal": "BUILD_MUSCLE",
"onboardingCompleted": true,
"onboardingStep": 8,
"experience": "INTERMEDIATE",
"timezone": "Asia/Jakarta",
"username": "johndoe",
"avatarUrl": "https://cdn.fumai.app/..."
},
"nutritionGoal": {
"dailyCalories": 2200,
"proteinTarget": 150,
"carbsTarget": 250,
"fatTarget": 70,
"goalType": "MAINTAIN"
},
"subscription": {
"id": "clx...",
"planId": "clx...",
"status": "ACTIVE",
"currentPeriodStart": "2026-03-01T...",
"currentPeriodEnd": "2026-04-01T..."
},
"stats": {
"totalWorkouts": 42,
"totalFoodEntries": 156
}
}
}Peringatan: Response menggunakan key
user(bukandata). Nullable fields:referralCode,nutritionGoal,subscription,profile.
Error Codes
| Code | Status | Keterangan |
|---|---|---|
UNAUTHORIZED | 401 | Authorization header tidak ada |
INVALID_ACCESS_TOKEN | 401 | Access token tidak valid atau expired |
USER_NOT_FOUND | 404 | User tidak ditemukan |
SERVER_ERROR | 500 | Internal server error |
POST /auth/logout
Logout user dan revoke token.
Memerlukan JWT authentication.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
refreshToken | string | Tidak | Refresh token untuk di-revoke (single device) |
allDevices | boolean | Tidak | true = logout semua device |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "eyJhbGciOi..." }'const res = await fetch('https://fumai.app/api/mobile/v1/auth/logout', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
})GET /auth/sessions
Mendapatkan daftar active sessions (device yang sedang login).
Memerlukan JWT authentication.
Contoh Request
curl -X GET https://fumai.app/api/mobile/v1/auth/sessions \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"const res = await fetch('https://fumai.app/api/mobile/v1/auth/sessions', {
headers: { Authorization: `Bearer ${accessToken}` },
})
const data = await res.json()Response 200
{
"success": true,
"data": {
"sessions": [
{
"id": "clx...",
"deviceInfo": "Samsung Galaxy S24",
"deviceName": "Galaxy S24",
"platform": "android",
"appVersion": "1.0.0",
"ipAddress": "103.xxx.xxx.xxx",
"lastUsedAt": "2026-03-10T14:30:00Z",
"createdAt": "2026-03-05T10:00:00Z"
}
],
"totalSessions": 1
}
}POST /auth/sessions
Revoke session (logout device tertentu atau semua device lain).
Memerlukan JWT authentication.
Revoke Single Session
{
"action": "revoke_session",
"sessionId": "clx..."
}Revoke All (Kecuali Device Saat Ini)
{
"action": "revoke_all",
"exceptCurrentRefreshToken": "eyJhbGciOi..."
}Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/sessions \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "action": "revoke_session", "sessionId": "clx..." }'const res = await fetch('https://fumai.app/api/mobile/v1/auth/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ action: 'revoke_session', sessionId: 'clx...' }),
})Response 200
{
"success": true,
"message": "Sesi berhasil diakhiri"
}Error Codes
| Code | Status | Keterangan |
|---|---|---|
UNAUTHORIZED | 401 | Token tidak valid |
VALIDATION_ERROR | 400 | Input tidak valid |
NOT_FOUND | 404 | Session ID tidak ditemukan |
RATE_LIMITED | 429 | Terlalu banyak request |
Rate Limit: 3 per jam (per IP)
POST /auth/set-initial-password
Set password untuk user yang belum punya password (user OAuth atau email-only register).
Memerlukan JWT authentication.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
password | string | Ya | Min 8 karakter, harus ada huruf kecil + besar + angka |
confirmPassword | string | Tidak | Harus sama dengan password (jika dikirim) |
Contoh Request
curl -X POST https://fumai.app/api/mobile/v1/auth/set-initial-password \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"password": "NewPassword123",
"confirmPassword": "NewPassword123"
}'const res = await fetch('https://fumai.app/api/mobile/v1/auth/set-initial-password', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
password: 'NewPassword123',
confirmPassword: 'NewPassword123',
}),
})Response 200
{
"success": true,
"message": "Password berhasil dibuat. Kamu sekarang bisa login dengan email dan password."
}Error Codes
| Code | Status | Keterangan |
|---|---|---|
UNAUTHORIZED | 401 | Token tidak valid |
VALIDATION_ERROR | 400 | Password lemah atau tidak sama |
USER_NOT_FOUND | 404 | User tidak ditemukan |
PASSWORD_ALREADY_SET | 409 | User sudah punya password |
RATE_LIMITED | 429 | Terlalu banyak percobaan |
Rate Limit: 3 per jam (per IP)