Skip to Content

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

FieldTipeWajibKeterangan
emailstringYaEmail 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

FieldTipeKeterangan
existsbooleantrue jika email sudah terdaftar
methodstring"register", "credentials", "google", atau "apple"
hasGooglebooleanUser memiliki Google OAuth
hasApplebooleanUser memiliki Apple Sign In
isVerifiedbooleanEmail sudah diverifikasi
userIdstring?User ID (hanya jika isVerified: false)

Behavior

SkenariomethodAksi
Email belum terdaftarregisterAuto-register → kirim OTP → verify
User punya passwordcredentialsTampilkan form login
User hanya punya GooglegoogleTampilkan Google Sign In
User hanya punya AppleappleTampilkan Apple Sign In
Email belum verifiedcredentialsResend OTP → verify

Error Codes

CodeStatusKeterangan
INVALID_EMAIL400Format email tidak valid
SERVER_ERROR500Internal 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

FieldTipeWajibKeterangan
emailstringYaEmail user
passwordstringYaPassword user
deviceInfostringTidakDevice identifier
deviceNamestringTidakNama device
platformstringTidak"android" atau "ios"
appVersionstringTidakVersi 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: nutritionGoal bisa null jika user belum setup nutrition. profile bisa null jika profil belum dibuat.

Error Codes

CodeStatusKeterangan
VALIDATION_ERROR400Input tidak valid
INVALID_CREDENTIALS401Email atau password salah
GOOGLE_ACCOUNT401User terdaftar via Google, tidak punya password
EMAIL_NOT_VERIFIED403Email belum diverifikasi
RATE_LIMITED429Terlalu banyak percobaan
SERVER_ERROR500Internal 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

FieldTipeWajibKeterangan
emailstringYaEmail user (maks 254 karakter, auto-lowercase)
passwordstringTidakMin 8 karakter, harus ada huruf kecil + besar + angka. Jika tidak dikirim, akun dibuat tanpa password
namestringTidakMin 2, maks 100 karakter. Default: email prefix
referralCodestringTidakKode referral (auto-uppercase, kode invalid di-ignore)
deviceInfostringTidakDevice identifier
platformstringTidak"android" atau "ios"
appVersionstringTidakVersi 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

AksiDetail
Auto-loginJWT tokens langsung diberikan
Profile dibuatProfile otomatis dibuat dengan onboardingCompleted: false
OTP dikirimKode OTP verifikasi dikirim otomatis ke email
Referral codeJika valid → buat redemption. Jika invalid → di-ignore

Error Codes

CodeStatusKeterangan
VALIDATION_ERROR400Input tidak valid
EMAIL_EXISTS409Email sudah terdaftar
RATE_LIMITED429Terlalu banyak percobaan
SERVER_ERROR500Internal 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

FieldTipeWajibKeterangan
userIdstringYaUser ID dari register/check-email response
codestringYaKode 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

CodeStatusKeterangan
VALIDATION_ERROR400Input tidak valid
USER_NOT_FOUND404User tidak ditemukan
INVALID_CODE400Kode OTP salah atau kadaluarsa (24 jam)
RATE_LIMITED429Terlalu banyak percobaan
SERVER_ERROR500Internal 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

FieldTipeWajibKeterangan
emailstringYaEmail 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: true meskipun email tidak terdaftar (untuk keamanan). Kode OTP berlaku 24 jam.

Error Codes

CodeStatusKeterangan
VALIDATION_ERROR400Email tidak valid
EMAIL_FAILED500Gagal mengirim email
RATE_LIMITED429Terlalu banyak request
SERVER_ERROR500Internal server error

Rate Limit: 2 per 5 menit (per IP)


POST /auth/google

Login atau register menggunakan Google ID token.

Request Body

FieldTipeWajibKeterangan
idTokenstringYaGoogle ID token
deviceInfostringTidakDevice identifier
deviceNamestringTidakNama device
platformstringTidak"android" atau "ios"
appVersionstringTidakVersi 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

SkenarioAksiStatus
Email baruAuto-create user + profile201
Email terdaftar (verified)Link Google account + login200
Email terdaftar (unverified)Tolak — harus verify dulu401

Error Codes

CodeStatusKeterangan
INVALID_GOOGLE_TOKEN401Token tidak valid atau expired
GOOGLE_EMAIL_NOT_VERIFIED401Email Google belum diverifikasi
ACCOUNT_NOT_VERIFIED401Email Fumai belum diverifikasi
RATE_LIMITED429Terlalu banyak percobaan
SERVER_ERROR500Internal server error

Rate Limit: 10 per 15 menit (per IP)


POST /auth/apple

Login atau register menggunakan Apple identity token.

Request Body

FieldTipeWajibKeterangan
identityTokenstringYaApple identity token (JWT)
fullNameobjectTidak{ givenName, familyName } — hanya dikirim Apple pada pertama kali
emailstringTidakEmail dari Apple (hanya pada first auth)
authorizationCodestringTidakAuthorization code dari Apple
deviceInfostringTidakDevice identifier
platformstringTidak"android" atau "ios"
appVersionstringTidakVersi aplikasi

Peringatan: Apple hanya mengirim fullName dan email saat pertama kali user mengotorisasi app. Pada login berikutnya, hanya identityToken yang tersedia. Client harus menyimpan fullName pada 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

AspekGoogleApple
Profile imageYaTidak
NameSelalu tersediaHanya pada first auth
EmailSelalu real emailBisa relay email

Error Codes

CodeStatusKeterangan
INVALID_APPLE_TOKEN401Token tidak valid atau expired
ACCOUNT_NOT_VERIFIED401Email Fumai belum diverifikasi
APPLE_EMAIL_REQUIRED400User harus memberikan izin email
RATE_LIMITED429Terlalu banyak percobaan
SERVER_ERROR500Internal server error

Rate Limit: 10 per 15 menit (per IP)


POST /auth/refresh

Refresh access token menggunakan refresh token.

Request Body

FieldTipeWajibKeterangan
refreshTokenstringYaRefresh 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

CodeStatusKeterangan
VALIDATION_ERROR400Refresh token tidak dikirim
INVALID_REFRESH_TOKEN401Token tidak valid atau expired
REFRESH_FAILED401Gagal memperbarui token — user harus login ulang
RATE_LIMITED429Terlalu banyak permintaan
SERVER_ERROR500Internal 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 (bukan data). Nullable fields: referralCode, nutritionGoal, subscription, profile.

Error Codes

CodeStatusKeterangan
UNAUTHORIZED401Authorization header tidak ada
INVALID_ACCESS_TOKEN401Access token tidak valid atau expired
USER_NOT_FOUND404User tidak ditemukan
SERVER_ERROR500Internal server error

POST /auth/logout

Logout user dan revoke token.

Memerlukan JWT authentication.

Request Body

FieldTipeWajibKeterangan
refreshTokenstringTidakRefresh token untuk di-revoke (single device)
allDevicesbooleanTidaktrue = 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

CodeStatusKeterangan
UNAUTHORIZED401Token tidak valid
VALIDATION_ERROR400Input tidak valid
NOT_FOUND404Session ID tidak ditemukan
RATE_LIMITED429Terlalu 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

FieldTipeWajibKeterangan
passwordstringYaMin 8 karakter, harus ada huruf kecil + besar + angka
confirmPasswordstringTidakHarus 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

CodeStatusKeterangan
UNAUTHORIZED401Token tidak valid
VALIDATION_ERROR400Password lemah atau tidak sama
USER_NOT_FOUND404User tidak ditemukan
PASSWORD_ALREADY_SET409User sudah punya password
RATE_LIMITED429Terlalu banyak percobaan

Rate Limit: 3 per jam (per IP)

Last updated on