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/json and always Accept: application/json.
  • A success is {"data": …}.
  • A list is {"data": [...], "meta": {"next_cursor": "…"}}. Ask for the next page with ?cursor=<next_cursor>; null means 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 as 01j9x6k3m4q5r6s7t8v9w0x1y2.

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

  1. Sign-in and tokens: email and password, Google, Apple, email codes, refresh, sign-out.
  2. Account: the profile, email verification, password and email changes, deletion.
  3. Remote Config, Push notifications, Files, Data.
  4. Your backend: the Admin API, verifying tokens and webhooks.