Appearance
Authentication & Profile
Sign-up and log-in flow
Sign-up and log-in are the same flow. The phone number identifies the account.
App API
│ POST /auth/otp {phone} │
│────────────────────────────────────▶│ (no SMS in challenge environment)
│ 204 │
│◀────────────────────────────────────│
│ POST /auth/verify {phone, code:"000000", device}
│────────────────────────────────────▶│ creates user on first login
│ 200 {accessToken, refreshToken, user, isNewUser}
│◀────────────────────────────────────│
│ │
│ if user.profileComplete == false: │
│ GET /usernames/{u}/availability │
│ PATCH /me {displayName, username}│
│────────────────────────────────────▶│
│ 200 User {profileComplete: true} │
│◀────────────────────────────────────│POST /auth/otp
json
{ "phone": "+6580000001" }Always 204, whether or not the account exists. Rate limit: 3 per phone per 10 minutes.
POST /auth/verify
json
{
"phone": "+6580000001",
"code": "000000",
"device": { "platform": "ios", "name": "Ana's iPhone", "appVersion": "1.0 (12)" }
}Response 200:
json
{
"accessToken": "eyJhbGciOi…",
"accessTokenExpiresAt": "2026-10-22T14:18:11.482Z",
"refreshToken": "rt_9c2f…",
"deviceId": "01JB8ZD…",
"isNewUser": true,
"user": {
"id": "01JB8Z…",
"phone": "+6580000001",
"displayName": null,
"username": null,
"about": "",
"avatarUrl": null,
"profileComplete": false,
"privacy": { "lastSeen": "friends" }
}
}Errors: 401 OTP_INVALID (wrong code), 429 RATE_LIMITED.
One account can be signed in on several devices. Each sign-in creates a device with its own tokens.
Profile completion
Until profileComplete is true, the API rejects friend, conversation, and message endpoints with 403 PROFILE_INCOMPLETE. Your app must show profile setup first.
GET /usernames/{username}/availability
No Authorization needed (only X-Client-Key).
json
{ "username": "ana_souza", "available": false, "reason": "taken" }reason: taken, invalid (format), or reserved (for example echo, admin).
PATCH /me
json
{ "displayName": "Ana Souza", "username": "ana_souza", "about": "Available", "avatarMediaId": null }| Field | Rules |
|---|---|
displayName | 1–50 characters. Required to complete the profile. |
username | ^[a-z][a-z0-9_]{2,19}$. Unique. Required to complete the profile. Can be changed later. |
about | 0–140 characters. |
avatarMediaId | Media ID from an image upload (A2), or null to remove. |
Errors: 409 USERNAME_TAKEN, 400 VALIDATION_FAILED.
Friends receive a user.updated event when the profile changes.
GET /me
Returns the current user, including profileComplete and privacy.
PATCH /me/privacy
json
{ "lastSeen": "friends" }everyone, friends (default), or nobody. With nobody, others see neither "online" nor "last seen".
Token refresh
POST /auth/refresh
json
{ "refreshToken": "rt_9c2f…" }Returns a new accessToken and a new refreshToken. Save both before using either.
Rules for the app:
- Refresh when the access token expires in less than 60 seconds, or after a
401 UNAUTHENTICATED. - Allow only one refresh at a time. Other requests wait for it.
- If refresh fails with
401, the session is over: clear local data and show the welcome screen. - A refresh token is single use. If an old one is reused (for example after a crash during refresh), all tokens of that device are revoked.
Log out
POST /auth/logout
Revokes the device's tokens and removes its push token. 204.
Delete account
DELETE /me
Required by App Store Guideline 5.1.1(v). Show a confirmation before calling it. 204.
What happens:
- All devices are logged out. Tokens are revoked.
- Friendships, friend requests, and blocks are removed.
- The user leaves all groups.
- In existing conversations, other members see the sender as "Deleted account" (
isDeleted: true,displayName: "Deleted account", no avatar). Messages already sent remain visible to them. - The phone number and username become free again.