Moving from Firebase or API v1
API v2 replaces Firebase Authentication, Remote Config, Cloud Messaging registration, Storage and Firestore with one HTTP API, and it replaces API v1 of this service. Both moves can happen app release by app release: v1 keeps working next to v2, and the same accounts work in both.
From API v1
For apps that use /api/v1 today (such as Royal Circle). Users keep their accounts, passwords, points
and member IDs.
Keep users signed in
When the updated app starts and still holds a v1 token, trade it for v2 tokens once with
POST /auth/token/from-v1 and {"v1_token": "<v1 token>"}. The answer is a normal
sign-in (tokens and user). The v1 token stops working at once, so delete it from
the app. 401 auth/invalid-token means the v1 token had already expired or was signed out: show the
sign-in screen. Sensitive actions still ask for the password once, because the sign-in time stays the
v1 one.
// Flutter
final v1Token = await storage.read(key: 'v1_token');
if (v1Token != null) {
try {
final json = await call('POST', '/auth/token/from-v1', body: {'v1_token': v1Token});
await session.keep(json['data']); // as after a sign-in
} on ApiError {
// expired or revoked: the user signs in again
} finally {
await storage.delete(key: 'v1_token');
}
}
// React Native
const v1Token = await SecureStore.getItemAsync('v1_token');
if (v1Token) {
try {
const { data } = await call('POST', '/auth/token/from-v1', { v1_token: v1Token });
await keep(data); // as after a sign-in
} catch {
// expired or revoked: the user signs in again
} finally {
await SecureStore.deleteItemAsync('v1_token');
}
}
// iOS
if let v1Token = Keychain.read("v1_token") {
defer { Keychain.delete("v1_token") }
if let json = try? await call("POST", "/auth/token/from-v1", body: ["v1_token": v1Token]) {
await session.keep(json["data"] as! [String: Any]) // as after a sign-in
}
}
// Android
legacyStore.read()?.let { v1Token ->
try {
session.keep(call("POST", "/auth/token/from-v1", JSONObject().put("v1_token", v1Token)).getJSONObject("data"))
} catch (e: ApiError) {
// expired or revoked: the user signs in again
} finally {
legacyStore.write(null)
}
}
// Web
const v1Token = localStorage.getItem('v1_token');
if (v1Token) {
localStorage.removeItem('v1_token');
try {
keep((await call('POST', '/auth/token/from-v1', { v1_token: v1Token })).data);
} catch {
// expired or revoked: the user signs in again
}
}
Endpoints
| v1 | v2 | Differences |
|---|---|---|
POST /register |
POST /auth/sign-up |
Signs in at once (tokens in the answer); names are optional; no password_confirmation |
POST /login |
POST /auth/sign-in |
Access token plus refresh token instead of one token; see keeping users signed in |
GET /me |
GET /me, GET /me/loyalty, GET /config |
The profile, the loyalty status and the app content are three calls |
POST /logout |
POST /auth/sign-out |
|
POST /password/forgot, POST /password/reset |
POST /auth/password/forgot, POST /auth/password/reset |
No password_confirmation; the reset ends every session |
DELETE /account/delete |
DELETE /me |
Needs a recent sign-in |
Also new in v2: Google and Apple sign-in, email codes, email verification, sessions, push, files and data.
Answers
- v1 answers
{"success": true, "app": …, "data": {…}}and errors{"message": "…"}; v2 answers{"data": …}and errors{"error": {"code", "message", "fields"}}. Handle errors bycode(overview). - Users are identified by
uid(a ULID string), not the numericid. - The content v1
/loginand/mereturn next to the user (telegram_url,tiers,exclusive_offers,events,services,cards,payment_infrastructure,concierge,meta_ads) are Remote Config parameters with the same names and values: fetch them withGET /config. Objects and lists come as JSON strings; parse them. The app's team edits them on the dashboard's Remote Config page, without a release.
Points, tier and member ID
GET /me/loyalty answers what v1 put in user:
{ "data": { "member_id": "PC-000123", "points": 3250, "tier": "gold", "next_tier": { "tier": "platinum", "points_needed": 6750 } } }
next_tier is null at the top tier. GET /me/loyalty/history lists the points added and taken away,
newest first, in pages (points, description, created_at). Both need a signed-in user.
When v1 goes
v1 keeps working until the service owner turns it off. When a date is set, every v1 answer carries a
Sunset header with it (and a Link to this page), so you see it coming in your HTTP logs. Move before
then; the owner checks that no users are left on v1 first.
From Firebase
The calls map one to one; the Firebase SDK is only kept for getting push tokens.
| Firebase | This API |
|---|---|
createUserWithEmailAndPassword |
POST /auth/sign-up |
signInWithEmailAndPassword |
POST /auth/sign-in |
signInWithCredential (Google, Apple) |
POST /auth/google, POST /auth/apple with the provider's ID token |
sendSignInLinkToEmail / signInWithEmailLink |
POST /auth/email-code/send / POST /auth/email-code/verify (a 6-digit code, and a magic link to your deep link) |
sendPasswordResetEmail / confirmPasswordReset |
POST /auth/password/forgot / POST /auth/password/reset (a code instead of a link) |
sendEmailVerification |
POST /auth/email/verification/send, then …/confirm with the code |
getIdToken(true) |
POST /auth/token/refresh |
onAuthStateChanged, currentUser |
Your session helper (example) and GET /me |
signOut |
POST /auth/sign-out |
updateProfile, updatePassword, verifyBeforeUpdateEmail |
PATCH /me, PUT /me/password, POST /me/email |
reauthenticateWithCredential |
POST /auth/reauthenticate |
linkWithCredential, unlink |
POST / DELETE /me/identities/{provider} |
user.delete() |
DELETE /me |
Remote Config fetchAndActivate, getValue |
GET /config (cache the values and the ETag yourself) |
Messaging getToken, subscribeToTopic |
Firebase's getToken, then POST /push/devices, PUT /push/devices/{id}/topics/{topic} |
Storage putFile, getDownloadURL, delete |
POST /files, GET /files/{id}/url (or public_url), DELETE /files/{id} |
Firestore add, set, update, get, delete |
POST, PUT, PATCH, GET, DELETE under /data/{collection} |
Firestore where, orderBy, limit, startAfter |
POST /data/{collection}/query |
Admin verifyIdToken |
Offline with the JWKS, or POST /admin/tokens/verify |
Admin getUser, createUser, updateUser, deleteUser |
/admin/users |
Admin setCustomUserClaims, revokeRefreshTokens |
PUT /admin/users/{uid}/claims, POST /admin/users/{uid}/revoke-sessions |
Admin messaging().send |
POST /admin/push/send |
Auth triggers (onCreate, onDelete) |
Webhooks |
What works differently:
- No anonymous or phone sign-in. Offer email codes instead of phone numbers.
- Security rules become a collection's access mode; anything finer runs in your backend through the Admin API.
- Firestore: no subcollections, transactions or realtime listeners (
onSnapshot); re-read or query when the screen opens, or send a push when data changes. Merges (PATCH) merge nested objects; there are no dot-path updates or field transforms. - Accounts can't be imported with their Firebase passwords. Existing users sign in with an email code (their email is verified by it) or with Google or Apple (linked by their verified email) and can set a new password in the app.