Wizgu Auth API
Wizgu Auth gives your app accounts and sign-in, Remote Config, push notifications, file storage and a JSON document store. It is a self-hosted replacement for the Firebase services of the same names, used over plain HTTPS from any platform.
This site has guides with examples for Flutter, React Native, iOS (Swift), Android (Kotlin), web apps (TypeScript) and PHP backends, plus the full API reference (also available as an OpenAPI file for code generators).
Two APIs
| Client API | Admin API | |
|---|---|---|
| Called by | Your app, on the user's device | Your backend server |
| Base URL | https://auth.wizgu.com/api/v2 |
https://auth.wizgu.com/api/v2/admin |
| Identifies the app with | X-App-Key: <app key> |
the server key |
| Identifies the user with | Authorization: Bearer <access token> |
a uid in the URL or body |
| Guide | Sign-in and tokens | Your backend |
The app key (a UUID) is on the app's page in the dashboard. It is not a secret: it only says which
app is calling, like Firebase's API key. A server key (wz_sk_…) is a secret: keep it on your
server and never ship it in an app.
Requests and answers
- Send JSON bodies with
Content-Type: application/jsonand alwaysAccept: application/json. - A success is
{"data": …}. - A list is
{"data": [...], "meta": {"next_cursor": "…"}}. Ask for the next page with?cursor=<next_cursor>;nullmeans there are no more.?limit=sets the page size (1–100, default 20). - Times are ISO 8601 in UTC, such as
2026-09-27T10:15:00.000000Z. - Users are identified by
uid, a lowercase ULID such as01j9x6k3m4q5r6s7t8v9w0x1y2.
Errors
Every error has the same shape:
{
"error": {
"code": "validation/failed",
"message": "The given data was invalid.",
"fields": { "email": ["The email field must be a valid email address."] }
}
}
Act on code, which is stable; message is for people and may change. fields is only there for
invalid input. Common codes:
| Status | Code | What to do |
|---|---|---|
| 401 | app/missing-key, app/invalid-key |
Check the X-App-Key header |
| 403 | app/disabled |
The app was deactivated in the dashboard |
| 403 | app/user-limit-reached |
The app takes no new accounts right now; existing users still sign in |
| 401 | auth/invalid-token |
Sign in again |
| 401 | auth/token-expired |
Refresh the tokens, then retry |
| 401 | auth/refresh-token-revoked |
The session ended: sign in again |
| 403 | auth/requires-recent-login |
Ask for the password again, then retry |
| 403 | auth/email-not-verified |
The app requires a verified email |
| 403 | permission-denied |
Not allowed |
| 404 | not-found |
No such endpoint or record |
| 422 | validation/failed |
Show the messages in fields |
| 429 | too-many-requests |
Wait for the seconds in the Retry-After header |
| 500 | internal |
Try again later |
Each endpoint's own codes are in the API reference.
Rate limits
Client calls are limited per user (or per IP before sign-in); sign-in, sign-up, codes and password
endpoints have stricter limits. The Admin API allows 600 requests a minute per server key. A limited
call answers 429 with a Retry-After header.
A minimal client
Each guide builds on a small helper that sends the app key and, when signed in, the access token. The sign-in guide adds token storage and refresh.
// Flutter: package:http
import 'dart:convert';
import 'package:http/http.dart' as http;
const baseUrl = 'https://auth.wizgu.com/api/v2';
const appKey = 'YOUR-APP-KEY';
Future<Map<String, dynamic>> call(String method, String path,
{Map<String, dynamic>? body, String? accessToken}) async {
final request = http.Request(method, Uri.parse('$baseUrl$path'))
..headers.addAll({
'X-App-Key': appKey,
'Accept': 'application/json',
'Content-Type': 'application/json',
if (accessToken != null) 'Authorization': 'Bearer $accessToken',
});
if (body != null) request.body = jsonEncode(body);
final response = await http.Response.fromStream(await request.send());
final json = response.body.isEmpty ? <String, dynamic>{} : jsonDecode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 400) {
throw ApiError(json['error']['code'] as String, json['error']['message'] as String);
}
return json;
}
class ApiError implements Exception {
ApiError(this.code, this.message);
final String code;
final String message;
}
// React Native: fetch is built in
const BASE_URL = 'https://auth.wizgu.com/api/v2';
const APP_KEY = 'YOUR-APP-KEY';
export class ApiError extends Error {
constructor(public code: string, message: string) { super(message); }
}
export async function call<T = any>(method: string, path: string, body?: unknown, accessToken?: string): Promise<T> {
const response = await fetch(BASE_URL + path, {
method,
headers: {
'X-App-Key': APP_KEY,
Accept: 'application/json',
'Content-Type': 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = response.status === 204 ? {} : await response.json();
if (!response.ok) throw new ApiError(json.error.code, json.error.message);
return json;
}
// iOS: URLSession
import Foundation
let baseURL = URL(string: "https://auth.wizgu.com/api/v2")!
let appKey = "YOUR-APP-KEY"
struct ApiError: Error { let code: String; let message: String }
func call(_ method: String, _ path: String, body: [String: Any]? = nil, accessToken: String? = nil) async throws -> [String: Any] {
var request = URLRequest(url: baseURL.appendingPathComponent(path))
request.httpMethod = method
request.setValue(appKey, forHTTPHeaderField: "X-App-Key")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") }
if let body { request.httpBody = try JSONSerialization.data(withJSONObject: body) }
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as! HTTPURLResponse).statusCode
let json = data.isEmpty ? [:] : (try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:])
if status >= 400, let error = json["error"] as? [String: Any] {
throw ApiError(code: error["code"] as? String ?? "internal", message: error["message"] as? String ?? "")
}
return json
}
// Android: OkHttp and org.json (call from a coroutine on Dispatchers.IO)
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
const val BASE_URL = "https://auth.wizgu.com/api/v2"
const val APP_KEY = "YOUR-APP-KEY"
val http = OkHttpClient()
class ApiError(val code: String, message: String) : Exception(message)
fun call(method: String, path: String, body: JSONObject? = null, accessToken: String? = null): JSONObject {
val request = Request.Builder()
.url(BASE_URL + path)
.header("X-App-Key", APP_KEY)
.header("Accept", "application/json")
.apply { if (accessToken != null) header("Authorization", "Bearer $accessToken") }
.method(method, body?.toString()?.toRequestBody("application/json".toMediaType()))
.build()
http.newCall(request).execute().use { response ->
val text = response.body?.string().orEmpty()
val json = if (text.isEmpty()) JSONObject() else JSONObject(text)
if (!response.isSuccessful) {
val error = json.getJSONObject("error")
throw ApiError(error.getString("code"), error.getString("message"))
}
return json
}
}
// Web: fetch. Allow your site's origin in the service's CORS settings.
const BASE_URL = 'https://auth.wizgu.com/api/v2';
const APP_KEY = 'YOUR-APP-KEY';
export class ApiError extends Error {
constructor(public code: string, message: string) { super(message); }
}
export async function call<T = any>(method: string, path: string, body?: unknown, accessToken?: string): Promise<T> {
const response = await fetch(BASE_URL + path, {
method,
headers: {
'X-App-Key': APP_KEY,
Accept: 'application/json',
'Content-Type': 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = response.status === 204 ? {} : await response.json();
if (!response.ok) throw new ApiError(json.error.code, json.error.message);
return json as T;
}
Where to next
- Sign-in and tokens: email and password, Google, Apple, email codes, refresh, sign-out.
- Account: the profile, email verification, password and email changes, deletion.
- Remote Config, Push notifications, Files, Data.
- Your backend: the Admin API, verifying tokens and webhooks.