Step-by-step for beginners, intermediate, and advanced. Keys, Firebase, device registration, SSE (not WebSocket), webhooks, and every challenge model with a live phone demo.
Keys
Most confusion comes from mixing these. Memorize the table, then pick your level.
Three secrets. Three jobs. Never mix them.
Project API key
ZINDUA_API_KEY = znd_live_…
Where: Dashboard → Project → API keys. Used for: Your backend creates challenges with Authorization: Bearer …. Never put this key inside the mobile APK / IPA or a public frontend.
Realtime
People often say WebSocket. PushMirror uses Server-Sent Events for the browser, and signed HTTP webhooks for backends.
SSE for the login page. Webhooks for your server. Not WebSocket.
SSE
The browser opens EventSource on GET /api/v1/challenges/:id/stream. When the phone responds, the login page unlocks in milliseconds. This is not a WebSocket — it is Server-Sent Events (one-way stream).
Live demo
Switch emoji, digit, or approve. Same production sheet.
Interactive schematic
Same sheet as production. Approve triggers the celebration burst.
New machine sign-in
Tap the same icon on your trusted device.
Your app
PushMirror · now
Chrome · Kinshasa · just now
Challenge models
Pick one type per login. The interactive demo above switches the same UI.
Pick one type per login. The interactive demo switches the same UI.
Emoji match
Login: Login shows one emoji (😄)
Phone: Phone shows 3–5 emojis — user taps the same one. Harder to phish by reading a code aloud
Choose your level
Start beginner even if you ship mobile apps. Intermediate adds Firebase. Advanced hardens production.
Dashboard + playground, no mobile code yet
Dashboard + playground, no mobile code yet
Create a Zindua project
Sign in → create a project → copy znd_live_… (or znd_test_…). Same key as email and WhatsApp.
Code path
Server creates → browser listens (SSE) → phone registers device → phone responds → optional webhook.
create-challenge.ts
import { Zindua } from "@zindua/sdk"; const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! }); // Called when someone signs in from a NEW machineconst challenge = await zindua.pushMirror.create({ userId: "usr_123", type: "emoji", // "emoji" | "approve" | "digit" purpose: "login", context: { device: "Chrome on Windows", city: "Kinshasa", }, fallback: { channel: "whatsapp", to: "+243832499559" },}); // Login page shows challenge.display (😄 or 42)// Branding comes from Push settings (app name + logo or PushMirror default)return { challengeId: challenge.id, display: challenge.display, branding: challenge.branding, // if your SDK surfaces it};Also on Developers
Short Confirm section with key placement and a downloadable example. This mobile page is the deep guide.
Create a project, paste your FCM key under Push → Config, register a device token, then create your first challenge.