Skip to Content

FitMatch

Fitur matching Fumai: temukan partner olahraga, workout buddy, atau koneksi baru. Semua endpoint memerlukan JWT authentication.

Info: FitMatch bersifat opsional. Saat membuat profil FitMatch, data otomatis di-sync dari profil utama user (bio, foto, tinggi, kota, olahraga, ketersediaan).


Profile

GET /fitmatch/profile

Mendapatkan profil FitMatch sendiri.

curl -X GET https://fumai.app/api/mobile/v1/fitmatch/profile \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/profile', { headers: { Authorization: `Bearer ${accessToken}` }, }) const data = await res.json()

Response 200

{ "success": true, "exists": true, "data": { "id": "clx...", "displayName": "John", "bio": "Fitness enthusiast", "birthDate": "1995-06-15", "gender": "MALE", "height": 175, "city": "Denpasar", "district": "Kuta", "latitude": -8.6705, "longitude": 115.2126, "age": 30, "status": "ACTIVE", "profileScore": 85, "discoverTutorialSeen": true, "lookingFor": ["FEMALE"], "interestedIn": ["WORKOUT_BUDDY"], "ageMin": 22, "ageMax": 32, "maxDistance": 25, "fitnessGoals": ["MUSCLE_GAIN"], "preferredTimes": ["MORNING", "EVENING"], "workoutDays": ["MONDAY", "WEDNESDAY", "FRIDAY"], "photos": [ { "id": "...", "url": "https://cdn.fumai.app/...", "isMain": true, "sortOrder": 0 } ], "prompts": [ { "id": "...", "question": "My go-to workout is...", "answer": "Morning run" } ], "venues": [ { "id": "...", "name": "Gold's Gym", "branch": "Kuta", "city": "Denpasar", "latitude": -8.7, "longitude": 115.17, "placeId": "ChIJ...", "chainId": "golds-gym", "isCustom": false, "sortOrder": 0 } ] } }

Info: Field exists di top-level akan false jika user belum membuat profil FitMatch.


POST /fitmatch/profile

Membuat profil FitMatch. Semua field auto-sync dari Profile — body minimal bisa {} untuk memakai semua data dari profil utama.

Request Body (Minimal)

Body bisa kosong jika user sudah punya data lengkap di profil utama:

{}

Info: displayName, birthDate, dan gender otomatis di-fill dari profil Fumai user. Hanya perlu dikirim jika user ingin override atau data belum ada di profil.

Request Body (Full — Override Auto-sync)

FieldTipeWajibKeterangan
displayNamestringTidakOverride nama tampilan
birthDatestringTidakOverride tanggal lahir (ISO date)
genderstringTidakOverride gender
biostringTidakBio khusus FitMatch
heightnumberTidakTinggi dalam cm
lookingForstring[]TidakGender yang dicari (contoh: ["FEMALE"])
interestedInstring[]TidakDari opsi fitmatch_interest
ageMinnumberTidakUsia minimum filter
ageMaxnumberTidakUsia maksimum filter
maxDistancenumberTidakRadius maks dalam km
fitnessGoalsstring[]TidakContoh: ["MUSCLE_GAIN", "STAY_FIT"]
preferredTimesstring[]TidakContoh: ["MORNING", "EVENING"]
workoutDaysstring[]TidakContoh: ["MONDAY", "WEDNESDAY"]
citystringTidakNama kota
districtstringTidakNama district/kecamatan
promptsobject[]TidakQ&A untuk profil: [{ question, answer }]

Auto-sync dari Profile

Field FitMatchSumber Profile
displayNameUser.name
birthDateProfile.birthDate
genderProfile.gender (MALE/FEMALE)
bioProfile.bio
heightProfile.height (rounded ke Int cm)
cityCity.nameLocal dari Profile.cityId
latitude/longitudeProfile.latitude/longitude
preferredTimesProfile.preferredAvailability (morning→MORNING, dst.)
photosProfile.photos[]FitMatchPhoto[]
sportsProfile.favoriteSports[]SportPreference[]
curl -X POST https://fumai.app/api/mobile/v1/fitmatch/profile \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "lookingFor": ["FEMALE"], "interestedIn": ["workout_buddy", "dating"], "ageMin": 20, "ageMax": 30, "maxDistance": 15 }'
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/profile', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ lookingFor: ['FEMALE'], interestedIn: ['workout_buddy', 'dating'], ageMin: 20, ageMax: 30, maxDistance: 15, }), })

PUT /fitmatch/profile

Update profil FitMatch (partial update).

curl -X PUT https://fumai.app/api/mobile/v1/fitmatch/profile \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "bio": "Updated bio", "maxDistance": 20 }'

Refresh Location Flow

Modal Refresh Location di Settings memanggil endpoint ini dengan koordinat city yang baru dipilih, supaya proximity filter punya anchor point yang fresh. Semua field opsional, dikirim hanya saat user re-pick city.

{ "city": "Surabaya, East Java", "latitude": -7.2575, "longitude": 112.7521 }

DELETE /fitmatch/profile

Hapus profil FitMatch.

curl -X DELETE https://fumai.app/api/mobile/v1/fitmatch/profile \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

GET /fitmatch/profile/:profileId

Lihat profil FitMatch user lain. Response termasuk compatibility score dan match status.

curl -X GET https://fumai.app/api/mobile/v1/fitmatch/profile/clx... \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Photos

GET /fitmatch/photos

Mendapatkan daftar foto FitMatch.

curl -X GET https://fumai.app/api/mobile/v1/fitmatch/photos \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

POST /fitmatch/photos

Upload foto FitMatch baru.

Content-Type: multipart/form-data
FieldTipeWajibKeterangan
fileFileYaImage file (maks 5MB, JPEG/PNG/WebP)

Response 200

{ "success": true, "data": { "photo": { "id": "...", "url": "https://cdn.fumai.app/...", "isMain": false, "sortOrder": 2 } } }

Batasan: Min 2 foto, maks 6 foto.


PUT /fitmatch/photos

Reorder foto atau set foto utama.

{ "photoIds": ["id1", "id2", "id3"] }

atau:

{ "mainPhotoId": "id1" }

DELETE /fitmatch/photos

Hapus foto.

ParameterTipeKeterangan
idstringPhoto ID (query parameter)
curl -X DELETE "https://fumai.app/api/mobile/v1/fitmatch/photos?id=PHOTO_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Discover & Swipe

GET /fitmatch/discover

Mendapatkan profil untuk di-discover/swipe.

ParameterTipeKeterangan
limitnumberJumlah profil (default: 10)
excludeIdsstringComma-separated ID yang dikecualikan
curl -X GET "https://fumai.app/api/mobile/v1/fitmatch/discover?limit=10" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/discover?limit=10', { headers: { Authorization: `Bearer ${accessToken}` }, }) const data = await res.json()

Response 200

{ "success": true, "data": { "profiles": [ { "id": "...", "displayName": "Jane", "age": 25, "gender": "FEMALE", "bio": "...", "city": "Denpasar, Bali", "distance": 5.2, "compatibilityScore": 85, "isPhotoVerified": true, "isOnline": true, "fitnessLevel": "INTERMEDIATE", "fitnessGoals": ["STAY_FIT"], "preferredTimes": ["MORNING"], "workoutDays": ["MONDAY", "FRIDAY"], "height": 168, "weight": 60, "favoriteSports": ["GYM_FITNESS", "RUNNING", "BADMINTON"], "photos": [{ "url": "...", "isMain": true }], "prompts": [{ "question": "...", "answer": "..." }], "venues": [ { "id": "...", "name": "Gold's Gym", "branch": "Kuta", "city": "Denpasar", "latitude": -8.7, "longitude": 115.17, "placeId": "ChIJ...", "chainId": "golds-gym", "isCustom": false } ] } ], "remaining": 8, "limits": { "dailySwipes": 10, "remaining": 8, "used": 2 } } }

Info: weight dan favoriteSports berasal dari profil onboarding utama kandidat (joined via user.profile). venues.latitude/longitude/placeId diisi oleh FitMatch setup wizard via Google Places.

Daily Limits: Free tier 10 swipe/hari, Plus tier unlimited.

Profile Gating

Endpoint discover memerlukan profil dengan status ACTIVE dan minimal 2 foto. Jika tidak terpenuhi, API mengembalikan:

CodeStatusKeterangan
INSUFFICIENT_PHOTOS400Profil belum ACTIVE atau kurang dari 2 foto

POST /fitmatch/swipe

Swipe pada profil.

Request Body

FieldTipeWajibKeterangan
targetProfileIdstringYaID profil target
swipeTypestringYa"PASS", "LIKE", atau "WORKOUT_BUDDY"
Swipe TypeKeterangan
PASSTidak tertarik
LIKETertarik (romantis)
WORKOUT_BUDDYTertarik (partner olahraga)
curl -X POST https://fumai.app/api/mobile/v1/fitmatch/swipe \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "targetProfileId": "clx...", "swipeType": "LIKE" }'
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/swipe', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ targetProfileId: 'clx...', swipeType: 'LIKE' }), }) const data = await res.json()

Response — Match!

{ "success": true, "data": { "swipeType": "LIKE", "isMatch": true, "match": { "matchId": "clx...", "matchType": "LIKE", "matchedAt": "2026-03-31T...", "matchedProfile": { "id": "...", "displayName": "Jane", "age": 25, "photo": "https://..." } }, "remaining": 7 } }

Matches & Chat

GET /fitmatch/matches

Mendapatkan daftar match.

ParameterTipeKeterangan
typestring"new" (tanpa pesan), "conversations" (ada pesan), kosong (semua)
curl -X GET "https://fumai.app/api/mobile/v1/fitmatch/matches?type=new" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/matches?type=new', { headers: { Authorization: `Bearer ${accessToken}` }, }) const data = await res.json()

Response 200

{ "success": true, "data": { "newMatches": [ { "matchId": "...", "matchType": "LIKE", "matchedAt": "...", "profile": { "displayName": "Jane", "age": 25, "photos": [] } } ], "conversations": [ { "matchId": "...", "profile": { "displayName": "Bob" }, "lastMessage": { "content": "Hey!", "createdAt": "...", "isMine": false }, "unreadCount": 2 } ], "totalMatches": 5 } }

GET /fitmatch/chat/:matchId

Mendapatkan chat room dengan pagination.

ParameterTipeKeterangan
cursorstringMessage ID untuk pagination
limitnumberJumlah pesan (default: 50)
curl -X GET "https://fumai.app/api/mobile/v1/fitmatch/chat/MATCH_ID?limit=50" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Info: Otomatis menandai pesan yang belum dibaca sebagai read.


POST /fitmatch/chat/:matchId/messages

Kirim pesan.

Text Message

{ "content": "Hey, want to workout together?" }

Image Message

{ "image": { "base64": "...", "mimeType": "image/jpeg" }, "content": "Check this out" }

Batasan: Maks 1000 karakter, image maks 5MB. Rate limit: 60/menit.

curl -X POST https://fumai.app/api/mobile/v1/fitmatch/chat/MATCH_ID/messages \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "content": "Hey, want to workout together?" }'
const res = await fetch( `https://fumai.app/api/mobile/v1/fitmatch/chat/${matchId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ content: 'Hey, want to workout together?' }), } )

GET /fitmatch/chat/:matchId/messages

Poll pesan baru.

ParameterTipeKeterangan
afterstringISO timestamp, ambil pesan setelah waktu ini

POST /fitmatch/chat/:matchId/read

Tandai pesan sebagai dibaca.


DELETE /fitmatch/chat/:matchId

Unmatch — hapus match dan semua pesan.


POST /fitmatch/chat/:matchId/typing

Kirim indikator sedang mengetik.

{ "isTyping": true }

Info: Kirim isTyping: true saat mulai mengetik, isTyping: false saat berhenti (debounce 3 detik direkomendasikan).


Venues

Tempat latihan/olahraga — gym, lapangan, court, taman, dll. (sebelumnya endpoint gyms).

GET /fitmatch/profile/venues

Mendapatkan daftar venue yang ditambahkan ke profil.

Response 200

{ "success": true, "data": { "venues": [ { "id": "...", "chainId": "golds-gym", "name": "Gold's Gym", "branch": "Kuta", "city": "Denpasar", "latitude": -8.7, "longitude": 115.17, "placeId": "ChIJ...", "isCustom": false, "sortOrder": 0 } ], "maxVenues": 5 } }

POST /fitmatch/profile/venues

Tambah venue ke profil.

{ "chainId": "golds-gym", "name": "Gold's Gym", "branch": "Kuta", "city": "Denpasar", "latitude": -8.7, "longitude": 115.17, "placeId": "ChIJ...", "isCustom": false }

Untuk custom venue (taman, lapangan tanpa chain):

{ "name": "My Home Gym", "isCustom": true }

Info: chainId mengacu ke library gym-chain. Pass null untuk venue non-gym.


PUT /fitmatch/profile/venues

Reorder atau update venue.

Reorder: { "venueIds": ["id1", "id2", "id3"] }

Update: { "id": "venueRecordId", "name": "Updated Name", "branch": "New Branch", "latitude": -8.7, "longitude": 115.17 }


DELETE /fitmatch/profile/venues

Hapus venue dari profil.

ParameterTipeKeterangan
idstringVenue record ID (query parameter)

Block & Report

POST /fitmatch/block

Block user. Otomatis: block user, deactivate match, hapus pending swipe.

{ "blockedProfileId": "clx...", "reason": "Inappropriate behavior" }

GET /fitmatch/block

Mendapatkan daftar user yang di-block.


DELETE /fitmatch/block

Unblock user.

ParameterTipeKeterangan
profileIdstringProfile ID (query parameter)

POST /fitmatch/report

Laporkan user.

Request Body

FieldTipeWajibKeterangan
reportedProfileIdstringYaID profil yang dilaporkan
reasonstringYaAlasan (lihat tabel)
detailsstringTidakDetail tambahan
evidencestring[]TidakURL screenshot bukti

Alasan Report

ReasonKeterangan
FAKE_PROFILEProfil palsu/catfish
HARASSMENTPelecehan atau bullying
INAPPROPRIATE_CONTENTFoto/pesan tidak pantas
SPAMSpam atau komersial
UNDERAGEUser di bawah 18 tahun
OFFLINE_BEHAVIORPerilaku tidak aman offline
OTHERAlasan lain

Peringatan: 3+ laporan atau alasan UNDERAGE → auto-suspend. Reporter otomatis mem-block user yang dilaporkan.


GET /fitmatch/report

Mendapatkan daftar laporan yang sudah dibuat.


Settings & Verification

GET /fitmatch/settings

Mendapatkan pengaturan FitMatch.


PUT /fitmatch/settings

Update pengaturan FitMatch.

{ "status": "ACTIVE", "showOnlineStatus": true, "showDistance": true, "showLastActive": true }
StatusKeterangan
ACTIVETerlihat di discover
PAUSEDTersembunyi dari discover

DELETE /fitmatch/settings

Hapus profil FitMatch secara permanen.

{ "confirmation": "DELETE_MY_FITMATCH" }

GET /fitmatch/verification

Cek status verifikasi foto.


POST /fitmatch/verification

Submit foto verifikasi (selfie).

Content-Type: multipart/form-data
FieldTipeKeterangan
photoFileSelfie (maks 5MB)

DELETE /fitmatch/verification

Batalkan permintaan verifikasi.


Badges

GET /fitmatch/badges

Mendapatkan badge dan progress badge.

Response 200

{ "success": true, "data": { "badges": ["CONSISTENT", "PR_ACHIEVER"], "progress": [ { "badge": "PHOTO_VERIFIED", "progress": 0, "requirement": "Submit verification photo" }, { "badge": "CONSISTENT", "earnedAt": "2026-03-15T...", "progress": 100 }, { "badge": "VETERAN", "progress": 60, "requirement": "1+ year on Fumai" } ] } }

Daftar Badge

BadgeSyarat
PHOTO_VERIFIEDSubmit dan lolos verifikasi foto
CONSISTENT4+ minggu streak workout
PR_ACHIEVER10+ personal records
VETERAN1+ tahun di Fumai
EARLY_ADOPTER1000 user pertama

POST /fitmatch/badges

Refresh/recalculate badge dari data workout terbaru.

curl -X POST https://fumai.app/api/mobile/v1/fitmatch/badges \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Setup Draft

Menyimpan state wizard setup FitMatch agar user bisa lanjutkan setelah menutup app di tengah flow. Disimpan di Profile.fitmatchSetupDraft (kolom JSON).

GET /fitmatch/setup-draft

Mendapatkan draft yang tersimpan.

curl -X GET https://fumai.app/api/mobile/v1/fitmatch/setup-draft \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/setup-draft', { headers: { Authorization: `Bearer ${accessToken}` }, }) const data = await res.json()

Response 200

{ "success": true, "data": { "draft": { "step": 4, "lookingFor": ["FEMALE"], "interestedIn": ["workout_buddy"], "ageMin": 22, "ageMax": 32, "maxDistance": 25, "location": "Denpasar, Bali", "locationLat": -8.6705, "locationLng": 115.2126, "venues": [ { "id": "ChIJ...", "name": "Gold's Gym", "address": "Kuta", "lat": -8.7, "lng": 115.17 } ], "savedAt": "2026-05-10T08:30:00.000Z" } } }

Info: data.draft adalah null jika belum ada draft tersimpan.


POST /fitmatch/setup-draft

Simpan draft. Server otomatis menambahkan savedAt.

Request Body

Partial wizard state apapun. Contoh:

{ "step": 4, "lookingFor": ["FEMALE"], "ageMin": 22, "ageMax": 32, "maxDistance": 25, "location": "Denpasar, Bali", "locationLat": -8.6705, "locationLng": 115.2126, "venues": [] }
curl -X POST https://fumai.app/api/mobile/v1/fitmatch/setup-draft \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "step": 4, "ageMin": 22, "ageMax": 32 }'
const res = await fetch('https://fumai.app/api/mobile/v1/fitmatch/setup-draft', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ step: 4, ageMin: 22, ageMax: 32 }), })

Response 200

{ "success": true, "data": { "draft": { "step": 4, "ageMin": 22, "ageMax": 32, "savedAt": "..." } } }

Info: Wizard biasanya menggunakan debounce ~800ms dan force-save pada setiap transisi Continue/Back.


DELETE /fitmatch/setup-draft

Hapus draft. Dipanggil setelah profil berhasil di-submit.

curl -X DELETE https://fumai.app/api/mobile/v1/fitmatch/setup-draft \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{ "success": true, "data": { "cleared": true } }

Tutorial Seen

Flag source-of-truth untuk Discover coach overlay (tutorial pertama kali). Disimpan di FitMatchProfile.discoverTutorialSeen agar tidak diulang lintas device. Mobile client sebaiknya mirror nilai ini di local storage untuk loading cepat.

GET /fitmatch/tutorial-seen

Cek apakah user sudah melihat tutorial.

curl -X GET https://fumai.app/api/mobile/v1/fitmatch/tutorial-seen \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{ "success": true, "data": { "seen": false } }

POST /fitmatch/tutorial-seen

Tandai tutorial sudah dilihat. Tidak perlu body.

curl -X POST https://fumai.app/api/mobile/v1/fitmatch/tutorial-seen \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 200

{ "success": true, "data": { "seen": true, "applied": true } }

Info: Jika user belum membuat profil FitMatch, applied akan false dan flag akan di-skip — flag akan diset saat user pertama kali membuka Discover setelah setup.

Info: Untuk force-replay tutorial, mobile client bisa implement local override (contoh: toggle “Replay tutorial” di Settings).

Last updated on