蜜恋后端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

25 KiB

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

{{base_url}}

Local development URL:

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:

Authorization: Bearer <access_token>

JSON APIs use Content-Type: application/json. File APIs use multipart/form-data.

1. Authentication

Health check

URL

GET {{base_url}}/health

Frontend sends: Nothing.

Backend returns

{"status": "ok"}

Register

URL

POST {{base_url}}/api/v1/auth/register

Frontend sends

{
  "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)

{
  "access_token": "<access_token>",
  "refresh_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

POST {{base_url}}/api/v1/auth/login

Frontend sends

{
  "email_or_phone": "user@example.com",
  "password": "password123"
}

Backend returns (200)

{
  "access_token": "<access_token>",
  "refresh_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

POST {{base_url}}/api/v1/auth/oauth

Frontend sends

{
  "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

POST {{base_url}}/api/v1/auth/refresh

Frontend sends

{"refresh_token": "<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

POST {{base_url}}/api/v1/auth/logout

Frontend sends

{"refresh_token": "<refresh_token>"}

Backend returns

{"message": "已退出登录"}

Current user

URL

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

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.

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

PATCH {{base_url}}/api/v1/users/me

Frontend sends (send only fields to change)

{
  "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

GET {{base_url}}/api/v1/users/me/interests

Frontend sends: Bearer access token, no body.

Backend returns

{"interests": ["Photography", "Dogs", "My custom interest"]}

Replace my interests

URL

PUT {{base_url}}/api/v1/users/me/interests

Frontend sends

{"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

PUT {{base_url}}/api/v1/users/me/location

Frontend sends

{"latitude": 31.2304, "longitude": 121.4737, "city": "Shanghai", "county": "Huangpu"}

Backend returns

{
  "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

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

{"id": 11, "media_type": "intro_video", "review_status": "pending"}

Upload profile image

URL

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

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

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.

URL

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

POST {{base_url}}/api/v1/meet/swipes

Frontend sends

{"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

{"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

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

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

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

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

POST {{base_url}}/api/v1/explore/posts

Frontend sends

{
  "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

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

GET {{base_url}}/api/v1/explore/posts/{post_id}

Frontend sends: Bearer token and post id.

Backend returns: PostResponse (200).

Delete post

URL

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

PATCH {{base_url}}/api/v1/explore/posts/{post_id}

Frontend sends: Bearer token, own post id, and only the fields to change in 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

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)

{
  "id": 20,
  "media_type": "image",
  "mime_type": "image/png",
  "size_bytes": 12345,
  "position": 0,
  "url": "https://<oss-host>/melove/posts/20/image.png?<signature>"
}

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

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

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

POST {{base_url}}/api/v1/explore/posts/{post_id}/like

Frontend sends: Bearer token and post id.

Backend returns (200): compact PostLikeResponse:

{
  "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

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

DELETE {{base_url}}/api/v1/explore/posts/{post_id}/like

Frontend sends: Bearer token and post id.

Backend returns (200): compact PostUnlikeResponse:

{
  "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

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

POST {{base_url}}/api/v1/explore/posts/{post_id}/comments

Frontend sends

{"content": "Nice post!"}

To reply to an existing comment, add parent_comment_id:

{"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

PATCH {{base_url}}/api/v1/explore/comments/{comment_id}

Frontend sends: Bearer token, own comment id.

{"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

DELETE {{base_url}}/api/v1/explore/comments/{comment_id}

Frontend sends: Bearer token and own comment id.

Backend returns: {"message": "评论已删除"}.

Like comment

URL

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

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

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:

{
  "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

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

PUT {{base_url}}/api/v1/feed/questions/{question_id}/answer

Frontend sends

{"answer": true}

For a text question:

{"answer": "Coffee and a walk"}

Backend returns

{"question_id": 1, "answer": true, "answered_at": "2026-09-01T10:00:00"}

Delete answer

URL

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

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

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

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

POST {{base_url}}/api/v1/chat/conversations/{conversation_id}/messages

Frontend sends

{
  "content": "Hello, nice to meet you!",
  "message_type": "text"
}

content is 1-5000 characters. message_type is currently text.

Backend returns (201)

{
  "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

DELETE {{base_url}}/api/v1/chat/messages/{message_id}

Frontend sends: Bearer token and own message id.

Backend returns

{
  "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 <access_token> 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.