# Dating App API - Frontend Integration List > Frontend integration reference listing each URL, request, and response. > API version: `0.6.12`. > Version 0.6.12 is deployed to production and verified through the public API. ## Base URL ```text {{base_url}} ``` Local development URL: ```text http://127.0.0.1:8001 ``` Replace `{{base_url}}` with the public backend URL when the backend is deployed. Do not use `127.0.0.1` from a user's phone or another computer. For protected APIs, send this header: ```text Authorization: Bearer ``` JSON APIs use `Content-Type: application/json`. File APIs use `multipart/form-data`. ## 1. Authentication ### Health check **URL** ```text GET {{base_url}}/health ``` **Frontend sends**: Nothing. **Backend returns** ```json {"status": "ok"} ``` ### Register **URL** ```text POST {{base_url}}/api/v1/auth/register ``` **Frontend sends** ```json { "email_or_phone": "user@example.com", "password": "password123", "confirm_password": "password123", "relationship_goal": "serious_relationship", "full_name": "Test User", "gender": "female", "birth_date": "2000-01-01", "city": "Shanghai", "country": "China", "bio": "I like traveling.", "interests": ["Photography", "Movies"] } ``` Required: `email_or_phone`, `password`, `confirm_password`, `relationship_goal`. The same `email_or_phone` field accepts either an email or a phone number. `confirm_password` must equal `password`; it is only used for validation and is not stored. `relationship_goal` is a single choice: `serious_relationship`, `casual_dates`, `new_friends`, or `marriage`. For the introduction step, the user chooses either a written bio or a video. Send a non-empty `bio` in this JSON for the written option. For the video option, omit `bio`, finish registration, then upload the file through `/api/v1/users/me/video` using the returned access token. After registration, call `/api/v1/auth/me`; `profile.profile_image_url` and `profile.video_url` are `null` until their corresponding media is uploaded. In production these fields contain directly loadable, one-hour OSS signed URLs. The introduction choices are mutually exclusive: uploading a video clears `bio`, and submitting a non-null `bio` later deletes the introduction video. The frontend should continue onboarding until exactly one of `bio` or `video_url` is non-null. **Backend returns** (`201`) ```json { "access_token": "", "refresh_token": "", "token_type": "bearer" } ``` Errors: `409` account already exists; `422` invalid or missing fields. MVP does not use OTP. The account is active immediately. ### Login **URL** ```text POST {{base_url}}/api/v1/auth/login ``` **Frontend sends** ```json { "email_or_phone": "user@example.com", "password": "password123" } ``` **Backend returns** (`200`) ```json { "access_token": "", "refresh_token": "", "token_type": "bearer", "user": { "id": 1, "email_or_phone": "user@example.com", "status": "active", "profile": null, "created_at": "2026-09-01T10:00:00" } } ``` Errors: `401` wrong account/password; `403` account suspended; `422` invalid fields. ### OAuth login/register **URL** ```text POST {{base_url}}/api/v1/auth/oauth ``` **Frontend sends** ```json { "provider": "google", "provider_user_id": "google-user-id-123", "email": "user@example.com", "full_name": "OAuth User" } ``` `provider` is `wechat` or `google`. `email` and `full_name` are optional. **Backend returns** (`200`): the same token and `user` structure as Login. ### Refresh token **URL** ```text POST {{base_url}}/api/v1/auth/refresh ``` **Frontend sends** ```json {"refresh_token": ""} ``` **Backend returns** (`200`): a new `access_token` and `refresh_token`, plus `token_type` and `user`. The old refresh token is invalid after rotation. ### Logout **URL** ```text POST {{base_url}}/api/v1/auth/logout ``` **Frontend sends** ```json {"refresh_token": ""} ``` **Backend returns** ```json {"message": "已退出登录"} ``` ### Current user **URL** ```text GET {{base_url}}/api/v1/auth/me ``` **Frontend sends**: Bearer access token, no body. **Backend returns** (`200`): `UserActivityResponse` with `id`, `email_or_phone`, `status`, `profile`, `created_at`, plus `posts`, `liked_users`, `liked_by_users`, and `matches`. These four arrays contain at most 20 items each. `liked_users_count`, `liked_by_users_count`, and `matches_count` are the full totals for active users, not the lengths of the limited arrays. ### Current user (compatible URL) **URL** ```text GET {{base_url}}/api/v1/users/me ``` **Frontend sends**: Bearer access token, no body. **Backend returns** (`200`): the same `UserActivityResponse` as `/api/v1/auth/me`. ### Profile-screen lists and public profiles All endpoints require a Bearer token. The post list endpoints use `limit` (1-50, default 20) and `offset` (0-5000, default 0). The match list and the two likes lists return every active item without pagination. ```text GET {{base_url}}/api/v1/users/me/posts?limit=20&offset=0 GET {{base_url}}/api/v1/users/me/liked GET {{base_url}}/api/v1/users/me/likes GET {{base_url}}/api/v1/users/me/matches GET {{base_url}}/api/v1/users/{user_id} GET {{base_url}}/api/v1/users/{user_id}/posts?limit=20&offset=0 ``` `liked` means users the current user liked; `likes` means users who liked the current user. The two post endpoints return `PostResponse[]`; the public profile endpoint returns `PublicUserProfileResponse` with public profile fields, `interests`, and the newest 20 `posts`; the liked/likes endpoints return complete `DiscoveryUserResponse[]` lists that exclude users you have already matched with, and matches returns `MatchResponse[]`. Legacy `limit` and `offset` parameters on the two likes endpoints and the two match endpoints are ignored; both match endpoints return the complete list. The public profile response excludes email/phone, account status, likes, and matches. ## 2. User profile, interests, location and media ### Update profile **URL** ```text PATCH {{base_url}}/api/v1/users/me ``` **Frontend sends** (send only fields to change) ```json { "full_name": "New Name", "gender": "female", "birth_date": "2000-01-01", "city": "Beijing", "country": "China", "relationship_goal": "marriage", "bio": "Updated bio", "occupation": "Creative Director at Studio Drift", "education": "NYU Gallatin School", "height_cm": 175, "beliefs": "spiritual_agnostic", "smoking": "non_smoker", "drinking": "socially", "pets": "golden_retriever", "exercise": "active_4x_week", "interests": ["Travel", "Music"] } ``` Optional detail fields: `occupation` and `education` (max 120), `height_cm` (100-250), `beliefs` (max 120), `smoking` and `drinking` (max 50 each), `pets` (max 120), `exercise` (max 50). Send `null` to clear a field. `GET /api/v1/auth/me` returns all of them and a computed `age`. **Backend returns** (`200`): updated `UserResponse`. ### Get my interests **URL** ```text GET {{base_url}}/api/v1/users/me/interests ``` **Frontend sends**: Bearer access token, no body. **Backend returns** ```json {"interests": ["Photography", "Dogs", "My custom interest"]} ``` ### Replace my interests **URL** ```text PUT {{base_url}}/api/v1/users/me/interests ``` **Frontend sends** ```json {"interests": ["Photography", "Dogs", "My custom interest"]} ``` **Backend returns**: the updated `{"interests": [...]}`. At least 2 interests are required; there is no maximum. `Other` is sent as normal custom text. ### Update location **URL** ```text PUT {{base_url}}/api/v1/users/me/location ``` **Frontend sends** ```json {"latitude": 31.2304, "longitude": 121.4737, "city": "Shanghai", "county": "Huangpu"} ``` **Backend returns** ```json { "latitude": 31.2304, "longitude": 121.4737, "city": "Shanghai", "county": "Huangpu", "updated_at": "2026-09-01T10:00:00" } ``` All four fields are required. `county` is the district/county name, distinct from `country` (nation). The location call also updates the profile city used by Meet ranking; the current-user and public-user profile responses include `county`. ### Upload introduction video **URL** ```text POST {{base_url}}/api/v1/users/me/video ``` **Frontend sends**: `multipart/form-data`, file field `video`. Allowed: MP4/MOV/WebM, maximum 100 MB. Re-uploading successfully replaces the user's previous introduction video; the frontend does not need to delete the old one first. **Backend returns** ```json {"id": 11, "media_type": "intro_video", "review_status": "pending"} ``` ### Upload profile image **URL** ```text POST {{base_url}}/api/v1/users/me/image ``` **Frontend sends**: `multipart/form-data`, file field `image`. Allowed: JPG/PNG/WebP, maximum 10 MB. Re-uploading successfully replaces the user's previous profile image; the frontend does not need to delete the old one first. **Backend returns**: `{ "id": 12, "media_type": "profile_image", "review_status": "pending" }`. ### Upload identity document **URL** ```text POST {{base_url}}/api/v1/users/me/identity ``` **Frontend sends**: `multipart/form-data`, file field `document`. Allowed: JPG/PNG/PDF, maximum 10 MB. **Backend returns**: `{ "id": 13, "media_type": "identity_document", "review_status": "pending" }`. ### Get user media **URL** ```text GET {{base_url}}/api/v1/media/{media_id} ``` **Frontend sends**: Bearer access token and the `media_id` path value. **Backend returns**: file binary (`200`). Public profile media can be read by active users; identity documents can only be read by the owner. `403` means no permission; `404` means missing file. Normally the frontend should render `profile_image_url`, `video_url`, or `media[].url` from profile/discovery responses directly; production returns one-hour signed OSS URLs that do not need a Bearer header. Re-fetch the containing API response when a URL expires. ## 3. Meet, swipe, match and search ### Get recommended users **URL** ```text GET {{base_url}}/api/v1/meet?limit=20 ``` **Frontend sends**: Bearer access token. Optional `limit` is 1-50, default 20. **Backend returns**: array of public user objects containing `country`, `relationship_goal`, `compatibility_percentage`, a dedicated `profile_image_url`, optional `video_url`, and `media`. Production media URLs are complete one-hour OSS signed URLs and can be rendered without another token-bearing media request. Already-swiped users and inactive users are excluded. Users who sent the current user an active `super_like` appear first; the rest are ordered by same city, same country, then other countries, with higher compatibility first within each group. ### Swipe user **URL** ```text POST {{base_url}}/api/v1/meet/swipes ``` **Frontend sends** ```json {"target_user_id": 2, "direction": "like"} ``` `direction` is `pass` (left swipe), `like` (right swipe), or `super_like`. A Super Like counts as a like for matching and remains active until the sender undoes it or changes the swipe. It gives the sender top priority in the target user's Meet list unless the target has already swiped the sender. **Backend returns** ```json {"direction": "like", "matched": true, "match_id": 15} ``` When not matched, `match_id` is `null`. `400` cannot swipe yourself; `404` target not found. ### Cancel like **URL** ```text DELETE {{base_url}}/api/v1/meet/likes/{target_user_id} ``` **Frontend sends**: Bearer token and target user id in the URL. **Backend returns**: `{"message": "已取消喜欢"}`. The related match is also removed. This legacy endpoint accepts `like` and `super_like`; use the undo endpoint below for the Cancel button. ### Cancel button: undo any swipe **URL** ```text DELETE {{base_url}}/api/v1/meet/swipes/{target_user_id} ``` **Frontend sends**: Bearer token and target user id. This removes a saved `pass`, `like`, or `super_like`, so the target can appear in the sender's Meet list again. Undoing a Super Like immediately removes the sender's priority in the target's Meet list. Undoing a matched like or Super Like also removes that match. **Backend returns**: `{"message":"已撤销滑动","previous_direction":"super_like"}`. `previous_direction` is `pass`, `like`, or `super_like`. ### Get matches **URL** ```text GET {{base_url}}/api/v1/matches ``` **Frontend sends**: Bearer access token. No query parameters or request body are required; legacy `limit` and `offset` parameters are ignored. **Backend returns**: array of `{ "id": 15, "matched_at": "...", "user": { "...": "public user" } }`. ### Search users **URL** ```text GET {{base_url}}/api/v1/search?q=Bob&city=Shanghai&country=China&latitude=31.23&longitude=121.47&max_distance_km=5&limit=20 ``` **Frontend sends**: Bearer token. All query parameters are optional: `q` (search text), `city`, `country`, `latitude`, `longitude`, `max_distance_km` (0-500), and `limit` (1-50, default 20). `city` and `country` can be used separately or together. Latitude and longitude must be supplied together. **Backend returns**: array of public user objects containing `country` and `compatibility_percentage`, optionally filtered by distance. Location priority and compatibility are applied before distance ordering. ## 4. Explore posts ### Create post **URL** ```text POST {{base_url}}/api/v1/explore/posts ``` **Frontend sends** ```json { "content": "Feeling **happy** and *relaxed* today", "post_type": "text", "content_format": "markdown", "feeling": "happy", "location": {"name": "Blue Bottle Coffee, Soho NYC", "latitude": 40.723, "longitude": -73.998} } ``` `post_type` is `text`, `image`, or `video`. Text posts need content, feeling, or location; `content` is at most 5000 characters. `feeling` is an optional 1-50 character label. `location.name` is required when location is present; coordinates must be sent together or both omitted. The post location is a snapshot and does not update the user's profile. For B/I editing, put `**bold**` or `*italic*` in `content` and set `content_format` to `markdown`; otherwise use the default `plain`. Render Markdown safely on the frontend. Music is not supported yet. For image/video posts, create the post first and use the returned `id` with `POST /api/v1/explore/posts/{post_id}/media` to upload files. Up to nine images and one video are allowed together; `post_type` becomes `mixed` when both are present. **Backend returns** (`201`): PostResponse with `id`, `content`, `post_type`, `content_format`, `feeling`, `location`, `author`, `like_count`, `comment_count`, `liked_by_me`, `liked_by_users`, `media`, and `created_at`. The same fields appear in Explore, post detail, Get Me, and public-user post lists. Post and comment `author` objects contain `id`, `full_name`, `profile_image_url`, `city`, `country`, and `compatibility_percentage`. The avatar URL is `null` before upload; production returns a directly loadable one-hour OSS signed URL. Re-fetch the response after expiry. Every PostResponse (Explore, detail, profile-embedded posts, and user post lists) includes `liked_by_users: [{"id": 2, "full_name": "Reader"}]`. It is `[]` without likes, ordered newest like first, and contains active users only; `like_count` equals its length. Names can be `null`. This list contains no contact details or full user profiles. ### Get Explore feed **URL** ```text GET {{base_url}}/api/v1/explore/feed?limit=20&offset=0 ``` **Frontend sends**: Bearer token. `limit` is 1-50, default 20; use `offset` for pagination with location/compatibility ranking. The legacy `before_id` parameter remains available as a chronological cutoff and cannot be combined with a nonzero `offset`. **Backend returns**: array of PostResponse. Authors in the same city are first, followed by authors in the same country and then other countries; higher compatibility is first within each group. Recency breaks remaining ties. ### Get post detail **URL** ```text GET {{base_url}}/api/v1/explore/posts/{post_id} ``` **Frontend sends**: Bearer token and post id. **Backend returns**: PostResponse (`200`). ### Delete post **URL** ```text DELETE {{base_url}}/api/v1/explore/posts/{post_id} ``` **Frontend sends**: Bearer token and own post id. **Backend returns**: `{"message": "帖子已删除"}`. Only the author can delete a post. ### Edit post **URL** ```text PATCH {{base_url}}/api/v1/explore/posts/{post_id} ``` **Frontend sends**: Bearer token, own post id, and only the fields to change in JSON. ```json {"content": "Updated text", "content_format": "markdown", "feeling": "happy", "location": {"name": "New place"}} ``` Editable: `content` (max 5000), `content_format`, `feeling`, `location`. Omitted fields are unchanged; send `null` to clear `feeling` or `location`. **Backend returns** (`200`): full updated PostResponse. Errors: `403` not the author; `404` post not found; `422` invalid fields. ### Upload post media **URL** ```text POST {{base_url}}/api/v1/explore/posts/{post_id}/media?position=0 ``` **Frontend sends**: Bearer token, `multipart/form-data` file field `media`, optional `position` 0-8. Supports JPG/PNG/WebP/MP4/MOV/WebM. One post allows up to 9 images and 1 video together; `position` is 0-9. **Backend returns** (`201`) ```json { "id": 20, "media_type": "image", "mime_type": "image/png", "size_bytes": 12345, "position": 0, "url": "https:///melove/posts/20/image.png?" } ``` In production, `url` is a complete one-hour OSS signed URL and can be loaded without a Bearer header. Local development uses `/api/v1/explore/media/{media_id}`. ### Get post media **URL** ```text GET {{base_url}}/api/v1/explore/media/{media_id} ``` **Frontend sends**: Bearer token and media id. **Backend returns**: file binary (`200`). ### Delete post media **URL** ```text DELETE {{base_url}}/api/v1/explore/media/{media_id} ``` **Frontend sends**: Bearer token. Only the post author can delete it. **Backend returns**: `{"message": "帖子媒体已删除"}`. ### Like post **URL** ```text POST {{base_url}}/api/v1/explore/posts/{post_id}/like ``` **Frontend sends**: Bearer token and post id. **Backend returns** (`200`): compact PostLikeResponse: ```json { "message": "like successful", "like_count": 1, "liked_by_me": true, "liked_by_users": [{"id": 2, "full_name": "Reader"}] } ``` Repeated likes do not increase the count. This response no longer contains the full post body, author, or media. Update the existing post using the returned count, state, and list. ### List post likers **URL** ```text GET {{base_url}}/api/v1/explore/posts/{post_id}/like ``` **Frontend sends**: Bearer token and post id, no body or pagination parameters. **Backend returns** (`200`): `[{"id": 2, "full_name": "Reader"}]`, or `[]` without likes. Names can be `null`; only active users appear, newest like first. Errors: `401` unauthenticated; `404` post not found. ### Unlike post **URL** ```text DELETE {{base_url}}/api/v1/explore/posts/{post_id}/like ``` **Frontend sends**: Bearer token and post id. **Backend returns** (`200`): compact PostUnlikeResponse: ```json { "message": "unlike successful", "like_count": 0, "liked_by_me": false, "liked_by_users": [] } ``` Repeated requests keep the post unliked. This response does not contain the full post body, author, or media. ### List comments **URL** ```text GET {{base_url}}/api/v1/explore/posts/{post_id}/comments ``` **Frontend sends**: Bearer token and post id. **Backend returns**: a flat array of CommentResponse. Each item contains `id`, `post_id`, `content`, `author`, `parent_comment_id`, `reply_count`, `like_count`, `liked_by_me`, and `created_at`. Group replies by `parent_comment_id` on the frontend. ### Create comment **URL** ```text POST {{base_url}}/api/v1/explore/posts/{post_id}/comments ``` **Frontend sends** ```json {"content": "Nice post!"} ``` To reply to an existing comment, add `parent_comment_id`: ```json {"content": "I agree", "parent_comment_id": 12} ``` **Backend returns** (`201`): CommentResponse including `author.profile_image_url` (or `null` without an avatar), `parent_comment_id`, `reply_count`, `like_count`, and `liked_by_me`. Content must be 1-2000 characters and cannot be blank. ### Edit comment **URL** ```text PATCH {{base_url}}/api/v1/explore/comments/{comment_id} ``` **Frontend sends**: Bearer token, own comment id. ```json {"content": "Updated comment"} ``` **Backend returns** (`200`): updated CommentResponse. Content must be 1-2000 characters and cannot be blank. Errors: `403` not the author; `404` comment not found; `422` invalid content. ### Delete comment **URL** ```text DELETE {{base_url}}/api/v1/explore/comments/{comment_id} ``` **Frontend sends**: Bearer token and own comment id. **Backend returns**: `{"message": "评论已删除"}`. ### Like comment **URL** ```text POST {{base_url}}/api/v1/explore/comments/{comment_id}/like ``` **Frontend sends**: Bearer token and comment id, no body. **Backend returns** (`200`): `{"message": "like successful", "like_count": 1, "liked_by_me": true}`. Repeated likes do not increase the count. ### Unlike comment **URL** ```text DELETE {{base_url}}/api/v1/explore/comments/{comment_id}/like ``` **Frontend sends**: Bearer token and comment id, no body. **Backend returns** (`200`): `{"message": "unlike successful", "like_count": 0, "liked_by_me": false}`. ## 5. Feed questions ### List questions **URL** ```text GET {{base_url}}/api/v1/feed/questions?unanswered_only=false ``` **Frontend sends**: Bearer token. Optional `unanswered_only` is `true` or `false`. **Backend returns**: array of question objects: ```json { "id": 1, "code": "weekend_movies", "question_text": "Do you like watching movies on weekends?", "question_text_zh": "你喜欢周末看电影吗?", "answer_type": "boolean", "current_answer": null, "answered_at": null } ``` ### List my answers **URL** ```text GET {{base_url}}/api/v1/feed/answers/me ``` **Frontend sends**: Bearer token. **Backend returns**: array of the same question objects, containing the user's current answers. ### Answer or update question **URL** ```text PUT {{base_url}}/api/v1/feed/questions/{question_id}/answer ``` **Frontend sends** ```json {"answer": true} ``` For a text question: ```json {"answer": "Coffee and a walk"} ``` **Backend returns** ```json {"question_id": 1, "answer": true, "answered_at": "2026-09-01T10:00:00"} ``` ### Delete answer **URL** ```text DELETE {{base_url}}/api/v1/feed/questions/{question_id}/answer ``` **Frontend sends**: Bearer token and question id. **Backend returns**: `{"message": "回答已删除"}`. ## 6. Chat Chat is available only to the two users in an existing match. ### List conversations **URL** ```text GET {{base_url}}/api/v1/chat/conversations ``` **Frontend sends**: Bearer token. **Backend returns**: array of conversation objects containing `id`, `match_id`, `other_user`, `last_message`, `created_at`, and `updated_at`. ### Create or get conversation **URL** ```text POST {{base_url}}/api/v1/chat/conversations/{match_id} ``` **Frontend sends**: Bearer token and match id in the URL. No body. **Backend returns** (`201`): ConversationResponse. If it already exists, the existing conversation is returned. ### List messages **URL** ```text GET {{base_url}}/api/v1/chat/conversations/{conversation_id}/messages?limit=50&before_id=100 ``` **Frontend sends**: Bearer token. `limit` is 1-100, default 50; `before_id` is optional pagination cursor. **Backend returns**: array of ChatMessageResponse in time order. ### Send message **URL** ```text POST {{base_url}}/api/v1/chat/conversations/{conversation_id}/messages ``` **Frontend sends** ```json { "content": "Hello, nice to meet you!", "message_type": "text" } ``` `content` is 1-5000 characters. `message_type` is currently `text`. **Backend returns** (`201`) ```json { "id": 30, "conversation_id": 8, "sender_id": 1, "content": "Hello, nice to meet you!", "message_type": "text", "is_deleted": false, "created_at": "2026-09-01T10:00:00" } ``` ### Recall message **URL** ```text DELETE {{base_url}}/api/v1/chat/messages/{message_id} ``` **Frontend sends**: Bearer token and own message id. **Backend returns** ```json { "id": 30, "conversation_id": 8, "sender_id": 1, "content": null, "message_type": "text", "is_deleted": true, "created_at": "2026-09-01T10:00:00" } ``` ## 7. Frontend flow 1. Register or login and save `access_token` and `refresh_token`. 2. Send `Authorization: Bearer ` on protected APIs. 3. On app startup call `/api/v1/auth/me`. A `200` means the user can enter the app; a `401` means clear tokens and show login. 4. When access token expires, call `/api/v1/auth/refresh` with the refresh token and save the new pair. 5. Do not call OTP endpoints. `/api/v1/auth/request-code` and `/api/v1/auth/verify` do not exist in the MVP API. ## 8. API source Machine-readable OpenAPI file: `docs/openapi-0.6.12.json`.