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
existsdi top-level akanfalsejika 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, dangenderotomatis 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)
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
displayName | string | Tidak | Override nama tampilan |
birthDate | string | Tidak | Override tanggal lahir (ISO date) |
gender | string | Tidak | Override gender |
bio | string | Tidak | Bio khusus FitMatch |
height | number | Tidak | Tinggi dalam cm |
lookingFor | string[] | Tidak | Gender yang dicari (contoh: ["FEMALE"]) |
interestedIn | string[] | Tidak | Dari opsi fitmatch_interest |
ageMin | number | Tidak | Usia minimum filter |
ageMax | number | Tidak | Usia maksimum filter |
maxDistance | number | Tidak | Radius maks dalam km |
fitnessGoals | string[] | Tidak | Contoh: ["MUSCLE_GAIN", "STAY_FIT"] |
preferredTimes | string[] | Tidak | Contoh: ["MORNING", "EVENING"] |
workoutDays | string[] | Tidak | Contoh: ["MONDAY", "WEDNESDAY"] |
city | string | Tidak | Nama kota |
district | string | Tidak | Nama district/kecamatan |
prompts | object[] | Tidak | Q&A untuk profil: [{ question, answer }] |
Auto-sync dari Profile
| Field FitMatch | Sumber Profile |
|---|---|
displayName | User.name |
birthDate | Profile.birthDate |
gender | Profile.gender (MALE/FEMALE) |
bio | Profile.bio |
height | Profile.height (rounded ke Int cm) |
city | City.nameLocal dari Profile.cityId |
latitude/longitude | Profile.latitude/longitude |
preferredTimes | Profile.preferredAvailability (morning→MORNING, dst.) |
photos | Profile.photos[] → FitMatchPhoto[] |
sports | Profile.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| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
file | File | Ya | Image 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.
| Parameter | Tipe | Keterangan |
|---|---|---|
id | string | Photo 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.
| Parameter | Tipe | Keterangan |
|---|---|---|
limit | number | Jumlah profil (default: 10) |
excludeIds | string | Comma-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:
weightdanfavoriteSportsberasal dari profil onboarding utama kandidat (joined viauser.profile).venues.latitude/longitude/placeIddiisi 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:
| Code | Status | Keterangan |
|---|---|---|
INSUFFICIENT_PHOTOS | 400 | Profil belum ACTIVE atau kurang dari 2 foto |
POST /fitmatch/swipe
Swipe pada profil.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
targetProfileId | string | Ya | ID profil target |
swipeType | string | Ya | "PASS", "LIKE", atau "WORKOUT_BUDDY" |
| Swipe Type | Keterangan |
|---|---|
PASS | Tidak tertarik |
LIKE | Tertarik (romantis) |
WORKOUT_BUDDY | Tertarik (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.
| Parameter | Tipe | Keterangan |
|---|---|---|
type | string | "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.
| Parameter | Tipe | Keterangan |
|---|---|---|
cursor | string | Message ID untuk pagination |
limit | number | Jumlah 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.
| Parameter | Tipe | Keterangan |
|---|---|---|
after | string | ISO 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: truesaat mulai mengetik,isTyping: falsesaat 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:
chainIdmengacu ke library gym-chain. Passnulluntuk 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.
| Parameter | Tipe | Keterangan |
|---|---|---|
id | string | Venue 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.
| Parameter | Tipe | Keterangan |
|---|---|---|
profileId | string | Profile ID (query parameter) |
POST /fitmatch/report
Laporkan user.
Request Body
| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
reportedProfileId | string | Ya | ID profil yang dilaporkan |
reason | string | Ya | Alasan (lihat tabel) |
details | string | Tidak | Detail tambahan |
evidence | string[] | Tidak | URL screenshot bukti |
Alasan Report
| Reason | Keterangan |
|---|---|
FAKE_PROFILE | Profil palsu/catfish |
HARASSMENT | Pelecehan atau bullying |
INAPPROPRIATE_CONTENT | Foto/pesan tidak pantas |
SPAM | Spam atau komersial |
UNDERAGE | User di bawah 18 tahun |
OFFLINE_BEHAVIOR | Perilaku tidak aman offline |
OTHER | Alasan 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
}| Status | Keterangan |
|---|---|
ACTIVE | Terlihat di discover |
PAUSED | Tersembunyi 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| Field | Tipe | Keterangan |
|---|---|---|
photo | File | Selfie (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
| Badge | Syarat |
|---|---|
PHOTO_VERIFIED | Submit dan lolos verifikasi foto |
CONSISTENT | 4+ minggu streak workout |
PR_ACHIEVER | 10+ personal records |
VETERAN | 1+ tahun di Fumai |
EARLY_ADOPTER | 1000 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.draftadalahnulljika 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,
appliedakanfalsedan 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).