Account

Everything here is called as the signed-in user: X-App-Key plus Authorization: Bearer <access token>. The examples use the authorized helper from Sign-in and tokens.

Profile

GET /me returns the user; PATCH /me changes first_name and last_name (both optional; null clears one) and returns the user.

const { data: user } = await authorized('PATCH', '/me', { first_name: 'Jane', last_name: 'Doe' });

Profile photo

POST /me/photo uploads a photo as multipart field photo (JPEG, PNG, WebP or GIF, within the app's file limits). The photo is public; its stable URL is the user's photo_url and the picture claim of new access tokens. DELETE /me/photo removes it. To use an image the user already uploaded (Files), send PATCH /me with {"photo_file_id": "<file id>"}.

// Flutter
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/me/photo'))
  ..headers.addAll({'X-App-Key': appKey, 'Accept': 'application/json', 'Authorization': 'Bearer $accessToken'})
  ..files.add(await http.MultipartFile.fromPath('photo', imagePath));
final response = await http.Response.fromStream(await request.send());
// React Native
const form = new FormData();
form.append('photo', { uri: image.uri, name: 'photo.jpg', type: 'image/jpeg' } as any);
const response = await fetch(`${BASE_URL}/me/photo`, {
  method: 'POST',
  headers: { 'X-App-Key': APP_KEY, Accept: 'application/json', Authorization: `Bearer ${accessToken}` },
  body: form, // fetch sets the multipart Content-Type and boundary
});
// iOS
var request = URLRequest(url: baseURL.appendingPathComponent("me/photo"))
request.httpMethod = "POST"
let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue(appKey, forHTTPHeaderField: "X-App-Key")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

var body = Data()
body.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"photo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
body.append(jpegData)
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
let (data, _) = try await URLSession.shared.upload(for: request, from: body)
// Android: OkHttp
val body = MultipartBody.Builder().setType(MultipartBody.FORM)
    .addFormDataPart("photo", "photo.jpg", file.asRequestBody("image/jpeg".toMediaType()))
    .build()
val request = Request.Builder().url("$BASE_URL/me/photo")
    .header("X-App-Key", APP_KEY).header("Accept", "application/json")
    .header("Authorization", "Bearer $accessToken")
    .post(body).build()
http.newCall(request).execute().use { response -> /* JSONObject(response.body!!.string()) */ }
// Web
const form = new FormData();
form.append('photo', fileInput.files![0]);
const response = await fetch(`${BASE_URL}/me/photo`, {
  method: 'POST',
  headers: { 'X-App-Key': APP_KEY, Accept: 'application/json', Authorization: `Bearer ${accessToken}` },
  body: form,
});

Verifying the email address

Apps can require a verified email for their features (the require_verified_email setting, shown by GET /app); other calls then answer 403 auth/email-not-verified.

  1. POST /auth/email/verification/send emails a 6-digit code (valid 10 minutes).
  2. POST /auth/email/verification/confirm with {"code"} marks the address verified and returns the user.
  3. Refresh the tokens so the access token says email_verified: true.

Sign-in with Google, Apple or an email code gives a verified address from the start.

Sensitive actions

Deleting the account, changing the email and linking or unlinking sign-in methods need a sign-in in the last 5 minutes. Otherwise they answer 403 auth/requires-recent-login: ask for the password and call POST /auth/reauthenticate with {"password"}. It returns a new access token (the refresh token stays); then retry. Users without a password sign in again with their provider instead.

try {
  await authorized('DELETE', '/me');
} catch (e) {
  if (e instanceof ApiError && e.code === 'auth/requires-recent-login') {
    const { data } = await authorized('POST', '/auth/reauthenticate', { password: await askForPassword() });
    accessToken = data.access_token;
    await authorized('DELETE', '/me');
  } else throw e;
}

Changing the password

PUT /me/password with {"current_password", "password"} (204). The user's other sessions end; this one stays. A wrong current password answers 400 auth/invalid-credential.

Changing the email address

  1. POST /me/email with {"email"} (needs a recent sign-in) emails a code to the new address. 409 auth/email-already-in-use if another account of the app has it.
  2. POST /me/email/confirm with {"email", "code"} switches to the new, verified, address and returns the user.

Sign-in methods

GET /me/identities lists how the user can sign in: password, google, apple.

  • POST /me/identities/password with {"password"} adds a password (for an account created with Google, Apple or an email code).
  • POST /me/identities/google with {"id_token", "nonce"} and POST /me/identities/apple with {"identity_token", "nonce", "authorization_code"} link those providers. 409 auth/credential-already-in-use when that Google or Apple account signs in to another user.
  • DELETE /me/identities/{provider} unlinks one; the last method can't be removed (409 auth/last-sign-in-method).

Signed-in devices

GET /me/sessions lists the user's sessions (current marks this one) with the sign-in method, times, IP address and user agent. DELETE /me/sessions/{id} signs that device out.

Deleting the account

DELETE /me (needs a recent sign-in; optional {"reason"}) deletes the account: every session ends, its files and private documents are deleted, stored Apple tokens are revoked, and the email can sign up again. It answers 204. Offer it in the app: the App Store and Google Play require account deletion in apps that let users create accounts.