Appearance
Friends
Users must be friends before they can start a 1:1 chat. Friendship is mutual: one user sends a request, the other accepts.
Friendship states
Search results and user profiles include friendshipStatus from the caller's point of view:
friendshipStatus | Meaning | Action to show |
|---|---|---|
none | No relation | "Add friend" |
outgoing_pending | Caller sent a request | "Request sent" / "Cancel" |
incoming_pending | The other user sent a request | "Accept" / "Decline" |
friends | Mutual friends | "Message" |
self | The caller | — |
Blocked users never appear in search, so there is no blocked state there. See Blocking.
POST /friend-requests accept
none ───────────────────────────▶ pending ───────────────▶ friends
▲ │ │
│ decline / cancel │ │ DELETE /friends/{id}
└──────────────────────────────────┘ │ or block
▲ │
└────────────────────────────────────────────────────────────┘Find people
GET /users/search?q=
qstarting with+: exact phone match (E.164).- Otherwise: username prefix match, case-insensitive, minimum 2 characters.
- Returns at most 20 results. The caller is excluded.
json
{
"data": [
{
"id": "01JC…",
"displayName": "Ben Tan",
"username": "ben_tan",
"avatarUrl": null,
"about": "Hey there!",
"friendshipStatus": "none"
}
],
"page": { "nextCursor": null, "hasMore": false }
}phone is never returned in search results.
GET /users/{userId}
Profile with friendshipStatus. lastSeenAt and isOnline are included only for friends, and only if the user's privacy setting allows it.
Friend requests
POST /friend-requests
json
{ "userId": "01JC…" }| Situation | Response |
|---|---|
| New request | 201 with FriendRequest (status: "pending"). The other user gets friend_request.received. |
| The other user already sent you a request | 200 with FriendRequest (status: "accepted"). You are now friends. Both get friend_request.accepted. |
| Already friends | 409 ALREADY_FRIENDS |
| You already sent a request | 409 FRIEND_REQUEST_EXISTS |
| User not found, deleted, or blocked either way | 404 USER_NOT_FOUND |
json
{
"id": "01JD…",
"fromUser": { "id": "01JB…", "displayName": "Ana Souza", "username": "ana_souza", "avatarUrl": null },
"toUser": { "id": "01JC…", "displayName": "Ben Tan", "username": "ben_tan", "avatarUrl": null },
"status": "pending",
"createdAt": "2026-10-22T14:04:00.000Z"
}GET /friend-requests?direction=incoming|outgoing
Paginated pending requests, newest first. Use incoming for the Requests screen and the badge count.
POST /friend-requests/{id}/accept
Only the recipient. Returns 200 with the new Friend. Both users receive friend_request.accepted.
POST /friend-requests/{id}/decline
Only the recipient. 204. The sender is not notified; for them the request simply disappears from outgoing on the next refresh.
DELETE /friend-requests/{id}
Only the sender (cancel). 204. The recipient receives friend_request.cancelled.
Friends list
GET /friends
Paginated, sorted by displayName.
json
{
"data": [
{
"user": {
"id": "01JC…",
"displayName": "Ben Tan",
"username": "ben_tan",
"avatarUrl": null,
"about": "Hey there!",
"isOnline": true,
"lastSeenAt": "2026-10-22T14:03:11.482Z"
},
"since": "2026-10-22T14:05:00.120Z",
"directConversationId": "01JE…"
}
],
"page": { "nextCursor": null, "hasMore": false }
}directConversationId is null until one of you starts a chat.
DELETE /friends/{userId}
Removes the friendship for both users. 204. The other user receives friend.removed. The existing conversation stays readable, but new messages fail with 403 NOT_FRIENDS.
Blocking
POST /blocks
json
{ "userId": "01JC…" }204. Blocking:
- Removes the friendship and any pending requests between the two users.
- Hides each user from the other's search.
- Stops presence and typing events between them.
- Makes the other user's sends fail with
403 NOT_FRIENDS.
The blocked user is not notified.
GET /blocks
Paginated list of users the caller blocked (for the "Blocked users" settings screen).
DELETE /blocks/{userId}
204. Unblocking does not restore the friendship. A new request is needed.
Reporting
POST /reports
Required by App Store Guideline 1.2.
json
{
"targetType": "message",
"targetId": "01JF…",
"reason": "harassment",
"comment": "Optional free text"
}targetType: user or message. reason: spam, harassment, inappropriate, or other. Response 202. Show a confirmation, and offer to block the user.
Real-time events
| Event | Received by | When |
|---|---|---|
friend_request.received | Recipient | New request |
friend_request.accepted | Both users | Request accepted (including the auto-accept case) |
friend_request.cancelled | Recipient | Sender cancelled |
friend.removed | The other user | Friendship removed |
user.updated | All friends | Profile changed |
Payloads are in Real-time.